diff options
Diffstat (limited to 'src/plugins/multimedia/darwin/mediaplayer/avfmediaplayer.mm')
-rw-r--r-- | src/plugins/multimedia/darwin/mediaplayer/avfmediaplayer.mm | 1270 |
1 files changed, 1270 insertions, 0 deletions
diff --git a/src/plugins/multimedia/darwin/mediaplayer/avfmediaplayer.mm b/src/plugins/multimedia/darwin/mediaplayer/avfmediaplayer.mm new file mode 100644 index 000000000..964964a8e --- /dev/null +++ b/src/plugins/multimedia/darwin/mediaplayer/avfmediaplayer.mm @@ -0,0 +1,1270 @@ +// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies). +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "avfmediaplayer_p.h" +#include "avfvideorenderercontrol_p.h" +#include <avfvideosink_p.h> +#include <avfmetadata_p.h> + +#include "qaudiooutput.h" +#include "private/qplatformaudiooutput_p.h" + +#include <qpointer.h> +#include <QFileInfo> +#include <QtCore/qmath.h> +#include <QtCore/qmutex.h> + +#import <AVFoundation/AVFoundation.h> + +QT_USE_NAMESPACE + +//AVAsset Keys +static NSString* const AVF_TRACKS_KEY = @"tracks"; +static NSString* const AVF_PLAYABLE_KEY = @"playable"; + +//AVPlayerItem keys +static NSString* const AVF_STATUS_KEY = @"status"; +static NSString* const AVF_BUFFER_LIKELY_KEEP_UP_KEY = @"playbackLikelyToKeepUp"; + +//AVPlayer keys +static NSString* const AVF_RATE_KEY = @"rate"; +static NSString* const AVF_CURRENT_ITEM_KEY = @"currentItem"; +static NSString* const AVF_CURRENT_ITEM_DURATION_KEY = @"currentItem.duration"; + +static void *AVFMediaPlayerObserverRateObservationContext = &AVFMediaPlayerObserverRateObservationContext; +static void *AVFMediaPlayerObserverStatusObservationContext = &AVFMediaPlayerObserverStatusObservationContext; +static void *AVFMediaPlayerObserverPresentationSizeContext = &AVFMediaPlayerObserverPresentationSizeContext; +static void *AVFMediaPlayerObserverBufferLikelyToKeepUpContext = &AVFMediaPlayerObserverBufferLikelyToKeepUpContext; +static void *AVFMediaPlayerObserverTracksContext = &AVFMediaPlayerObserverTracksContext; +static void *AVFMediaPlayerObserverCurrentItemObservationContext = &AVFMediaPlayerObserverCurrentItemObservationContext; +static void *AVFMediaPlayerObserverCurrentItemDurationObservationContext = &AVFMediaPlayerObserverCurrentItemDurationObservationContext; + +@interface AVFMediaPlayerObserver : NSObject<AVAssetResourceLoaderDelegate> + +@property (readonly, getter=player) AVPlayer* m_player; +@property (readonly, getter=playerItem) AVPlayerItem* m_playerItem; +@property (readonly, getter=playerLayer) AVPlayerLayer* m_playerLayer; +@property (readonly, getter=session) AVFMediaPlayer* m_session; +@property (retain) AVPlayerItemTrack *videoTrack; + +- (AVFMediaPlayerObserver *) initWithMediaPlayerSession:(AVFMediaPlayer *)session; +- (void) setURL:(NSURL *)url mimeType:(NSString *)mimeType; +- (void) unloadMedia; +- (void) prepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys; +- (void) assetFailedToPrepareForPlayback:(NSError *)error; +- (void) playerItemDidReachEnd:(NSNotification *)notification; +- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object + change:(NSDictionary *)change context:(void *)context; +- (void) detatchSession; +- (void) dealloc; +- (BOOL) resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest; +@end + +#ifdef Q_OS_IOS +// Alas, no such thing as 'class variable', hence globals: +static unsigned sessionActivationCount; +static QMutex sessionMutex; +#endif // Q_OS_IOS + +@implementation AVFMediaPlayerObserver +{ +@private + AVFMediaPlayer *m_session; + AVPlayer *m_player; + AVPlayerItem *m_playerItem; + AVPlayerLayer *m_playerLayer; + NSURL *m_URL; + BOOL m_bufferIsLikelyToKeepUp; + NSData *m_data; + NSString *m_mimeType; +#ifdef Q_OS_IOS + BOOL m_activated; +#endif +} + +@synthesize m_player, m_playerItem, m_playerLayer, m_session; + +#ifdef Q_OS_IOS +- (void)setSessionActive:(BOOL)active +{ + const QMutexLocker lock(&sessionMutex); + if (active) { + // Don't count the same player twice if already activated, + // unless it tried to deactivate first: + if (m_activated) + return; + if (!sessionActivationCount) + [AVAudioSession.sharedInstance setActive:YES error:nil]; + ++sessionActivationCount; + m_activated = YES; + } else { + if (!sessionActivationCount || !m_activated) { + qWarning("Unbalanced audio session deactivation, ignoring."); + return; + } + --sessionActivationCount; + m_activated = NO; + if (!sessionActivationCount) + [AVAudioSession.sharedInstance setActive:NO error:nil]; + } +} +#endif // Q_OS_IOS + +- (AVFMediaPlayerObserver *) initWithMediaPlayerSession:(AVFMediaPlayer *)session +{ + if (!(self = [super init])) + return nil; + + m_session = session; + m_bufferIsLikelyToKeepUp = FALSE; + + m_playerLayer = [AVPlayerLayer playerLayerWithPlayer:nil]; + [m_playerLayer retain]; + m_playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill; + m_playerLayer.anchorPoint = CGPointMake(0.0f, 0.0f); + return self; +} + +- (void) setURL:(NSURL *)url mimeType:(NSString *)mimeType +{ + if (!m_session) + return; + + [m_mimeType release]; + m_mimeType = [mimeType retain]; + + if (m_URL != url) + { + [m_URL release]; + m_URL = [url copy]; + + //Create an asset for inspection of a resource referenced by a given URL. + //Load the values for the asset keys "tracks", "playable". + + // use __block to avoid maintaining strong references on variables captured by the + // following block callback +#if defined(Q_OS_IOS) + BOOL isAccessing = [m_URL startAccessingSecurityScopedResource]; +#endif + __block AVURLAsset *asset = [[AVURLAsset URLAssetWithURL:m_URL options:nil] retain]; + [asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()]; + + __block NSArray *requestedKeys = [[NSArray arrayWithObjects:AVF_TRACKS_KEY, AVF_PLAYABLE_KEY, nil] retain]; + + __block AVFMediaPlayerObserver *blockSelf = [self retain]; + + // Tells the asset to load the values of any of the specified keys that are not already loaded. + [asset loadValuesAsynchronouslyForKeys:requestedKeys completionHandler: + ^{ + dispatch_async( dispatch_get_main_queue(), + ^{ +#if defined(Q_OS_IOS) + if (isAccessing) + [m_URL stopAccessingSecurityScopedResource]; +#endif + [blockSelf prepareToPlayAsset:asset withKeys:requestedKeys]; + [asset release]; + [requestedKeys release]; + [blockSelf release]; + }); + }]; + } +} + +- (void) unloadMedia +{ + if (m_playerItem) { + [m_playerItem removeObserver:self forKeyPath:@"presentationSize"]; + [m_playerItem removeObserver:self forKeyPath:AVF_STATUS_KEY]; + [m_playerItem removeObserver:self forKeyPath:AVF_BUFFER_LIKELY_KEEP_UP_KEY]; + [m_playerItem removeObserver:self forKeyPath:AVF_TRACKS_KEY]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:AVPlayerItemDidPlayToEndTimeNotification + object:m_playerItem]; + m_playerItem = 0; + } + if (m_player) { + [m_player setRate:0.0]; + [m_player removeObserver:self forKeyPath:AVF_CURRENT_ITEM_DURATION_KEY]; + [m_player removeObserver:self forKeyPath:AVF_CURRENT_ITEM_KEY]; + [m_player removeObserver:self forKeyPath:AVF_RATE_KEY]; + [m_player release]; + m_player = 0; + } + if (m_playerLayer) + m_playerLayer.player = nil; +#if defined(Q_OS_IOS) + [self setSessionActive:NO]; +#endif +} + +- (void) prepareToPlayAsset:(AVURLAsset *)asset + withKeys:(NSArray *)requestedKeys +{ + if (!m_session) + return; + + //Make sure that the value of each key has loaded successfully. + for (NSString *thisKey in requestedKeys) + { + NSError *error = nil; + AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error]; +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << [thisKey UTF8String] << " status: " << keyStatus; +#endif + if (keyStatus == AVKeyValueStatusFailed) + { + [self assetFailedToPrepareForPlayback:error]; + return; + } + } + + //Use the AVAsset playable property to detect whether the asset can be played. +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << "isPlayable: " << [asset isPlayable]; +#endif + if (!asset.playable) + qWarning() << "Asset reported to be not playable. Playback of this asset may not be possible."; + + //At this point we're ready to set up for playback of the asset. + //Stop observing our prior AVPlayerItem, if we have one. + if (m_playerItem) + { + //Remove existing player item key value observers and notifications. + [self unloadMedia]; + } + + //Create a new instance of AVPlayerItem from the now successfully loaded AVAsset. + m_playerItem = [AVPlayerItem playerItemWithAsset:asset]; + if (!m_playerItem) { + qWarning() << "Failed to create player item"; + //Generate an error describing the failure. + NSString *localizedDescription = NSLocalizedString(@"Item cannot be played", @"Item cannot be played description"); + NSString *localizedFailureReason = NSLocalizedString(@"The assets tracks were loaded, but couldn't create player item.", @"Item cannot be played failure reason"); + NSDictionary *errorDict = [NSDictionary dictionaryWithObjectsAndKeys: + localizedDescription, NSLocalizedDescriptionKey, + localizedFailureReason, NSLocalizedFailureReasonErrorKey, + nil]; + NSError *assetCannotBePlayedError = [NSError errorWithDomain:@"StitchedStreamPlayer" code:0 userInfo:errorDict]; + + [self assetFailedToPrepareForPlayback:assetCannotBePlayedError]; + return; + } + + //Observe the player item "status" key to determine when it is ready to play. + [m_playerItem addObserver:self + forKeyPath:AVF_STATUS_KEY + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:AVFMediaPlayerObserverStatusObservationContext]; + + [m_playerItem addObserver:self + forKeyPath:@"presentationSize" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:AVFMediaPlayerObserverPresentationSizeContext]; + + [m_playerItem addObserver:self + forKeyPath:AVF_BUFFER_LIKELY_KEEP_UP_KEY + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:AVFMediaPlayerObserverBufferLikelyToKeepUpContext]; + + [m_playerItem addObserver:self + forKeyPath:AVF_TRACKS_KEY + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:AVFMediaPlayerObserverTracksContext]; + + //When the player item has played to its end time we'll toggle + //the movie controller Pause button to be the Play button + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(playerItemDidReachEnd:) + name:AVPlayerItemDidPlayToEndTimeNotification + object:m_playerItem]; + + //Get a new AVPlayer initialized to play the specified player item. + m_player = [AVPlayer playerWithPlayerItem:m_playerItem]; + [m_player retain]; + + //Set the initial volume on new player object + if (self.session) { + auto *audioOutput = m_session->m_audioOutput; + m_player.volume = (audioOutput ? audioOutput->volume : 1.); + m_player.muted = (audioOutput ? audioOutput->muted : true); + } + + //Assign the output layer to the new player + m_playerLayer.player = m_player; + + //Observe the AVPlayer "currentItem" property to find out when any + //AVPlayer replaceCurrentItemWithPlayerItem: replacement will/did + //occur. + [m_player addObserver:self + forKeyPath:AVF_CURRENT_ITEM_KEY + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:AVFMediaPlayerObserverCurrentItemObservationContext]; + + //Observe the AVPlayer "rate" property to update the scrubber control. + [m_player addObserver:self + forKeyPath:AVF_RATE_KEY + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:AVFMediaPlayerObserverRateObservationContext]; + + //Observe the duration for getting the buffer state + [m_player addObserver:self + forKeyPath:AVF_CURRENT_ITEM_DURATION_KEY + options:0 + context:AVFMediaPlayerObserverCurrentItemDurationObservationContext]; +#if defined(Q_OS_IOS) + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil]; + [self setSessionActive:YES]; +#endif +} + +-(void) assetFailedToPrepareForPlayback:(NSError *)error +{ + Q_UNUSED(error); + QMetaObject::invokeMethod(m_session, "processMediaLoadError", Qt::AutoConnection); +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; + qDebug() << [[error localizedDescription] UTF8String]; + qDebug() << [[error localizedFailureReason] UTF8String]; + qDebug() << [[error localizedRecoverySuggestion] UTF8String]; +#endif +} + +- (void) playerItemDidReachEnd:(NSNotification *)notification +{ + Q_UNUSED(notification); + if (self.session) + QMetaObject::invokeMethod(m_session, "processEOS", Qt::AutoConnection); +} + +- (void) observeValueForKeyPath:(NSString*) path + ofObject:(id)object + change:(NSDictionary*)change + context:(void*)context +{ + //AVPlayerItem "status" property value observer. + if (context == AVFMediaPlayerObserverStatusObservationContext) + { + AVPlayerStatus status = (AVPlayerStatus)[[change objectForKey:NSKeyValueChangeNewKey] integerValue]; + switch (status) + { + //Indicates that the status of the player is not yet known because + //it has not tried to load new media resources for playback + case AVPlayerStatusUnknown: + { + //QMetaObject::invokeMethod(m_session, "processLoadStateChange", Qt::AutoConnection); + } + break; + + case AVPlayerStatusReadyToPlay: + { + //Once the AVPlayerItem becomes ready to play, i.e. + //[playerItem status] == AVPlayerItemStatusReadyToPlay, + //its duration can be fetched from the item. + if (self.session) + QMetaObject::invokeMethod(m_session, "processLoadStateChange", Qt::AutoConnection); + } + break; + + case AVPlayerStatusFailed: + { + AVPlayerItem *playerItem = static_cast<AVPlayerItem*>(object); + [self assetFailedToPrepareForPlayback:playerItem.error]; + + if (self.session) + QMetaObject::invokeMethod(m_session, "processLoadStateFailure", Qt::AutoConnection); + } + break; + } + } else if (context == AVFMediaPlayerObserverPresentationSizeContext) { + QSize size(m_playerItem.presentationSize.width, m_playerItem.presentationSize.height); + QMetaObject::invokeMethod(m_session, "nativeSizeChanged", Qt::AutoConnection, Q_ARG(QSize, size)); + } else if (context == AVFMediaPlayerObserverBufferLikelyToKeepUpContext) + { + const bool isPlaybackLikelyToKeepUp = [m_playerItem isPlaybackLikelyToKeepUp]; + if (isPlaybackLikelyToKeepUp != m_bufferIsLikelyToKeepUp) { + m_bufferIsLikelyToKeepUp = isPlaybackLikelyToKeepUp; + QMetaObject::invokeMethod(m_session, "processBufferStateChange", Qt::AutoConnection, + Q_ARG(int, isPlaybackLikelyToKeepUp ? 100 : 0)); + } + } + else if (context == AVFMediaPlayerObserverTracksContext) + { + QMetaObject::invokeMethod(m_session, "updateTracks", Qt::AutoConnection); + } + //AVPlayer "rate" property value observer. + else if (context == AVFMediaPlayerObserverRateObservationContext) + { + //QMetaObject::invokeMethod(m_session, "setPlaybackRate", Qt::AutoConnection, Q_ARG(qreal, [m_player rate])); + } + //AVPlayer "currentItem" property observer. + //Called when the AVPlayer replaceCurrentItemWithPlayerItem: + //replacement will/did occur. + else if (context == AVFMediaPlayerObserverCurrentItemObservationContext) + { + AVPlayerItem *newPlayerItem = [change objectForKey:NSKeyValueChangeNewKey]; + if (m_playerItem != newPlayerItem) + m_playerItem = newPlayerItem; + } + else if (context == AVFMediaPlayerObserverCurrentItemDurationObservationContext) + { + const CMTime time = [m_playerItem duration]; + const qint64 duration = static_cast<qint64>(float(time.value) / float(time.timescale) * 1000.0f); + if (self.session) + QMetaObject::invokeMethod(m_session, "processDurationChange", Qt::AutoConnection, Q_ARG(qint64, duration)); + } + else + { + [super observeValueForKeyPath:path ofObject:object change:change context:context]; + } +} + +- (void) detatchSession +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + m_session = 0; +} + +- (void) dealloc +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + [self unloadMedia]; + + if (m_URL) { + [m_URL release]; + } + + [m_mimeType release]; + [m_playerLayer release]; + [super dealloc]; +} + +- (BOOL) resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest +{ + Q_UNUSED(resourceLoader); + + if (![loadingRequest.request.URL.scheme isEqualToString:@"iodevice"]) + return NO; + + QIODevice *device = m_session->mediaStream(); + if (!device) + return NO; + + device->seek(loadingRequest.dataRequest.requestedOffset); + if (loadingRequest.contentInformationRequest) { + loadingRequest.contentInformationRequest.contentType = m_mimeType; + loadingRequest.contentInformationRequest.contentLength = device->size(); + loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES; + } + + if (loadingRequest.dataRequest) { + NSInteger requestedLength = loadingRequest.dataRequest.requestedLength; + int maxBytes = qMin(32 * 1064, int(requestedLength)); + char buffer[maxBytes]; + NSInteger submitted = 0; + while (submitted < requestedLength) { + qint64 len = device->read(buffer, maxBytes); + if (len < 1) + break; + + [loadingRequest.dataRequest respondWithData:[NSData dataWithBytes:buffer length:len]]; + submitted += len; + } + + // Finish loading even if not all bytes submitted. + [loadingRequest finishLoading]; + } + + return YES; +} +@end + +AVFMediaPlayer::AVFMediaPlayer(QMediaPlayer *player) + : QObject(player), + QPlatformMediaPlayer(player), + m_state(QMediaPlayer::StoppedState), + m_mediaStatus(QMediaPlayer::NoMedia), + m_mediaStream(nullptr), + m_rate(1.0), + m_requestedPosition(-1), + m_duration(0), + m_bufferProgress(0), + m_videoAvailable(false), + m_audioAvailable(false), + m_seekable(false) +{ + m_observer = [[AVFMediaPlayerObserver alloc] initWithMediaPlayerSession:this]; + connect(&m_playbackTimer, &QTimer::timeout, this, &AVFMediaPlayer::processPositionChange); + setVideoOutput(new AVFVideoRendererControl(this)); +} + +AVFMediaPlayer::~AVFMediaPlayer() +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + //Detatch the session from the sessionObserver (which could still be alive trying to communicate with this session). + [m_observer detatchSession]; + [static_cast<AVFMediaPlayerObserver*>(m_observer) release]; +} + +void AVFMediaPlayer::setVideoSink(QVideoSink *sink) +{ + m_videoSink = sink ? static_cast<AVFVideoSink *>(sink->platformVideoSink()): nullptr; + m_videoOutput->setVideoSink(m_videoSink); +} + +void AVFMediaPlayer::setVideoOutput(AVFVideoRendererControl *output) +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << output; +#endif + + if (m_videoOutput == output) + return; + + //Set the current output layer to null to stop rendering + if (m_videoOutput) { + m_videoOutput->setLayer(nullptr); + } + + m_videoOutput = output; + + if (m_videoOutput && m_state != QMediaPlayer::StoppedState) + m_videoOutput->setLayer([static_cast<AVFMediaPlayerObserver*>(m_observer) playerLayer]); +} + +AVAsset *AVFMediaPlayer::currentAssetHandle() +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + AVAsset *currentAsset = [[static_cast<AVFMediaPlayerObserver*>(m_observer) playerItem] asset]; + return currentAsset; +} + +QMediaPlayer::PlaybackState AVFMediaPlayer::state() const +{ + return m_state; +} + +QMediaPlayer::MediaStatus AVFMediaPlayer::mediaStatus() const +{ + return m_mediaStatus; +} + +QUrl AVFMediaPlayer::media() const +{ + return m_resources; +} + +QIODevice *AVFMediaPlayer::mediaStream() const +{ + return m_mediaStream; +} + +static void setURL(AVFMediaPlayerObserver *observer, const QByteArray &url, const QString &mimeType = QString()) +{ + NSString *urlString = [NSString stringWithUTF8String:url.constData()]; + NSURL *nsurl = [NSURL URLWithString:urlString]; + [observer setURL:nsurl mimeType:[NSString stringWithUTF8String:mimeType.toLatin1().constData()]]; +} + +static void setStreamURL(AVFMediaPlayerObserver *observer, const QByteArray &url) +{ + setURL(observer, QByteArrayLiteral("iodevice://") + url, QFileInfo(QString::fromUtf8(url)).suffix()); +} + +void AVFMediaPlayer::setMedia(const QUrl &content, QIODevice *stream) +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << content.request().url(); +#endif + + [static_cast<AVFMediaPlayerObserver*>(m_observer) unloadMedia]; + + m_resources = content; + resetStream(stream); + + setAudioAvailable(false); + setVideoAvailable(false); + setSeekable(false); + m_requestedPosition = -1; + orientationChanged(QtVideo::Rotation::None, false); + Q_EMIT positionChanged(position()); + if (m_duration != 0) { + m_duration = 0; + Q_EMIT durationChanged(0); + } + if (!m_metaData.isEmpty()) { + m_metaData.clear(); + metaDataChanged(); + } + for (int i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i) { + tracks[i].clear(); + nativeTracks[i].clear(); + } + tracksChanged(); + + const QMediaPlayer::MediaStatus oldMediaStatus = m_mediaStatus; + const QMediaPlayer::PlaybackState oldState = m_state; + + if (!m_mediaStream && content.isEmpty()) { + m_mediaStatus = QMediaPlayer::NoMedia; + if (m_mediaStatus != oldMediaStatus) + Q_EMIT mediaStatusChanged(m_mediaStatus); + + m_state = QMediaPlayer::StoppedState; + if (m_state != oldState) + Q_EMIT stateChanged(m_state); + + return; + } + + m_mediaStatus = QMediaPlayer::LoadingMedia; + if (m_mediaStatus != oldMediaStatus) + Q_EMIT mediaStatusChanged(m_mediaStatus); + + if (m_mediaStream) { + // If there is a data, try to load it, + // otherwise wait for readyRead. + if (m_mediaStream->size()) + setStreamURL(m_observer, m_resources.toEncoded()); + } else { + //Load AVURLAsset + //initialize asset using content's URL + setURL(m_observer, m_resources.toEncoded()); + } + + m_state = QMediaPlayer::StoppedState; + if (m_state != oldState) + Q_EMIT stateChanged(m_state); +} + +qint64 AVFMediaPlayer::position() const +{ + AVPlayerItem *playerItem = [static_cast<AVFMediaPlayerObserver*>(m_observer) playerItem]; + + if (m_requestedPosition != -1) + return m_requestedPosition; + + if (!playerItem) + return 0; + + CMTime time = [playerItem currentTime]; + return static_cast<quint64>(float(time.value) / float(time.timescale) * 1000.0f); +} + +qint64 AVFMediaPlayer::duration() const +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + return m_duration; +} + +float AVFMediaPlayer::bufferProgress() const +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + return m_bufferProgress/100.; +} + +void AVFMediaPlayer::setAudioAvailable(bool available) +{ + if (m_audioAvailable == available) + return; + + m_audioAvailable = available; + Q_EMIT audioAvailableChanged(available); +} + +bool AVFMediaPlayer::isAudioAvailable() const +{ + return m_audioAvailable; +} + +void AVFMediaPlayer::setVideoAvailable(bool available) +{ + if (m_videoAvailable == available) + return; + + m_videoAvailable = available; + Q_EMIT videoAvailableChanged(available); +} + +bool AVFMediaPlayer::isVideoAvailable() const +{ + return m_videoAvailable; +} + +bool AVFMediaPlayer::isSeekable() const +{ + return m_seekable; +} + +void AVFMediaPlayer::setSeekable(bool seekable) +{ + if (m_seekable == seekable) + return; + + m_seekable = seekable; + Q_EMIT seekableChanged(seekable); +} + +QMediaTimeRange AVFMediaPlayer::availablePlaybackRanges() const +{ + AVPlayerItem *playerItem = [static_cast<AVFMediaPlayerObserver*>(m_observer) playerItem]; + + if (playerItem) { + QMediaTimeRange timeRanges; + + NSArray *ranges = [playerItem loadedTimeRanges]; + for (NSValue *timeRange in ranges) { + CMTimeRange currentTimeRange = [timeRange CMTimeRangeValue]; + qint64 startTime = qint64(float(currentTimeRange.start.value) / currentTimeRange.start.timescale * 1000.0); + timeRanges.addInterval(startTime, startTime + qint64(float(currentTimeRange.duration.value) / currentTimeRange.duration.timescale * 1000.0)); + } + if (!timeRanges.isEmpty()) + return timeRanges; + } + return QMediaTimeRange(0, duration()); +} + +qreal AVFMediaPlayer::playbackRate() const +{ + return m_rate; +} + +void AVFMediaPlayer::setAudioOutput(QPlatformAudioOutput *output) +{ + if (m_audioOutput == output) + return; + if (m_audioOutput) + m_audioOutput->q->disconnect(this); + m_audioOutput = output; + if (m_audioOutput) { + connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this, &AVFMediaPlayer::audioOutputChanged); + connect(m_audioOutput->q, &QAudioOutput::volumeChanged, this, &AVFMediaPlayer::setVolume); + connect(m_audioOutput->q, &QAudioOutput::mutedChanged, this, &AVFMediaPlayer::setMuted); + //connect(m_audioOutput->q, &QAudioOutput::audioRoleChanged, this, &AVFMediaPlayer::setAudioRole); + } + audioOutputChanged(); + setMuted(m_audioOutput ? m_audioOutput->muted : true); + setVolume(m_audioOutput ? m_audioOutput->volume : 1.); +} + +QMediaMetaData AVFMediaPlayer::metaData() const +{ + return m_metaData; +} + +void AVFMediaPlayer::setPlaybackRate(qreal rate) +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << rate; +#endif + + if (qFuzzyCompare(m_rate, rate)) + return; + + m_rate = rate; + + AVPlayer *player = [static_cast<AVFMediaPlayerObserver*>(m_observer) player]; + if (player && m_state == QMediaPlayer::PlayingState) + [player setRate:m_rate]; + + Q_EMIT playbackRateChanged(m_rate); +} + +void AVFMediaPlayer::setPosition(qint64 pos) +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << pos; +#endif + + if (pos == position()) + return; + + AVPlayerItem *playerItem = [static_cast<AVFMediaPlayerObserver*>(m_observer) playerItem]; + if (!playerItem) { + m_requestedPosition = pos; + Q_EMIT positionChanged(m_requestedPosition); + return; + } + + if (!isSeekable()) { + if (m_requestedPosition != -1) { + m_requestedPosition = -1; + Q_EMIT positionChanged(position()); + } + return; + } + + pos = qMax(qint64(0), pos); + if (duration() > 0) + pos = qMin(pos, duration()); + m_requestedPosition = pos; + + CMTime newTime = [playerItem currentTime]; + newTime.value = (pos / 1000.0f) * newTime.timescale; + [playerItem seekToTime:newTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero + completionHandler:^(BOOL finished) { + if (finished) + m_requestedPosition = -1; + }]; + + Q_EMIT positionChanged(pos); + + // Reset media status if the current status is EndOfMedia + if (m_mediaStatus == QMediaPlayer::EndOfMedia) { + QMediaPlayer::MediaStatus newMediaStatus = (m_state == QMediaPlayer::PausedState) ? QMediaPlayer::BufferedMedia + : QMediaPlayer::LoadedMedia; + Q_EMIT mediaStatusChanged((m_mediaStatus = newMediaStatus)); + } +} + +void AVFMediaPlayer::play() +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << "currently: " << m_state; +#endif + + if (m_mediaStatus == QMediaPlayer::NoMedia || m_mediaStatus == QMediaPlayer::InvalidMedia) + return; + + if (m_state == QMediaPlayer::PlayingState) + return; + + resetCurrentLoop(); + + if (m_videoOutput && m_videoSink) + m_videoOutput->setLayer([static_cast<AVFMediaPlayerObserver*>(m_observer) playerLayer]); + + // Reset media status if the current status is EndOfMedia + if (m_mediaStatus == QMediaPlayer::EndOfMedia) + setPosition(0); + + if (m_mediaStatus == QMediaPlayer::LoadedMedia || m_mediaStatus == QMediaPlayer::BufferedMedia) { + // Setting the rate starts playback + [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] setRate:m_rate]; + } + + m_state = QMediaPlayer::PlayingState; + processLoadStateChange(); + + Q_EMIT stateChanged(m_state); + m_playbackTimer.start(100); +} + +void AVFMediaPlayer::pause() +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << "currently: " << m_state; +#endif + + if (m_mediaStatus == QMediaPlayer::NoMedia) + return; + + if (m_state == QMediaPlayer::PausedState) + return; + + m_state = QMediaPlayer::PausedState; + + if (m_videoOutput && m_videoSink) + m_videoOutput->setLayer([static_cast<AVFMediaPlayerObserver*>(m_observer) playerLayer]); + + [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] pause]; + + // Reset media status if the current status is EndOfMedia + if (m_mediaStatus == QMediaPlayer::EndOfMedia) + setPosition(0); + + Q_EMIT positionChanged(position()); + Q_EMIT stateChanged(m_state); + m_playbackTimer.stop(); +} + +void AVFMediaPlayer::stop() +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << "currently: " << m_state; +#endif + + if (m_state == QMediaPlayer::StoppedState) + return; + + // AVPlayer doesn't have stop(), only pause() and play(). + [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] pause]; + setPosition(0); + + if (m_videoOutput) + m_videoOutput->setLayer(nullptr); + + if (m_mediaStatus == QMediaPlayer::BufferedMedia) + Q_EMIT mediaStatusChanged((m_mediaStatus = QMediaPlayer::LoadedMedia)); + + Q_EMIT stateChanged((m_state = QMediaPlayer::StoppedState)); + m_playbackTimer.stop(); +} + +void AVFMediaPlayer::setVolume(float volume) +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << volume; +#endif + + AVPlayer *player = [static_cast<AVFMediaPlayerObserver*>(m_observer) player]; + if (player) + player.volume = volume; +} + +void AVFMediaPlayer::setMuted(bool muted) +{ +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << muted; +#endif + + AVPlayer *player = [static_cast<AVFMediaPlayerObserver*>(m_observer) player]; + if (player) + player.muted = muted; +} + +void AVFMediaPlayer::audioOutputChanged() +{ +#ifdef Q_OS_MACOS + AVPlayer *player = [static_cast<AVFMediaPlayerObserver*>(m_observer) player]; + if (!m_audioOutput || m_audioOutput->device.id().isEmpty()) { + player.audioOutputDeviceUniqueID = nil; + if (!m_audioOutput) + player.muted = true; + } else { + NSString *str = QString::fromUtf8(m_audioOutput->device.id()).toNSString(); + player.audioOutputDeviceUniqueID = str; + } +#endif +} + +void AVFMediaPlayer::processEOS() +{ + if (doLoop()) { + setPosition(0); + [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] setRate:m_rate]; + return; + } + + //AVPlayerItem has reached end of track/stream +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO; +#endif + Q_EMIT positionChanged(position()); + m_mediaStatus = QMediaPlayer::EndOfMedia; + m_state = QMediaPlayer::StoppedState; + + if (m_videoOutput) + m_videoOutput->setLayer(nullptr); + + Q_EMIT mediaStatusChanged(m_mediaStatus); + Q_EMIT stateChanged(m_state); +} + +void AVFMediaPlayer::processLoadStateChange(QMediaPlayer::PlaybackState newState) +{ + AVPlayerStatus currentStatus = [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] status]; + +#ifdef QT_DEBUG_AVF + qDebug() << Q_FUNC_INFO << currentStatus << ", " << m_mediaStatus << ", " << newState; +#endif + + if (m_mediaStatus == QMediaPlayer::NoMedia) + return; + + if (currentStatus == AVPlayerStatusReadyToPlay) { + + QMediaPlayer::MediaStatus newStatus = m_mediaStatus; + + AVPlayerItem *playerItem = [m_observer playerItem]; + + // get the meta data + m_metaData = AVFMetaData::fromAsset(playerItem.asset); + Q_EMIT metaDataChanged(); + updateTracks(); + + if (playerItem) { + setSeekable([[playerItem seekableTimeRanges] count] > 0); + + // Get the native size of the video, and reset the bounds of the player layer + AVPlayerLayer *playerLayer = [m_observer playerLayer]; + if (m_observer.videoTrack && playerLayer) { + if (!playerLayer.bounds.size.width || !playerLayer.bounds.size.height) { + playerLayer.bounds = CGRectMake(0.0f, 0.0f, + m_observer.videoTrack.assetTrack.naturalSize.width, + m_observer.videoTrack.assetTrack.naturalSize.height); + } + } + + if (m_requestedPosition != -1) { + setPosition(m_requestedPosition); + m_requestedPosition = -1; + } + } + + newStatus = (newState != QMediaPlayer::StoppedState) ? QMediaPlayer::BufferedMedia + : QMediaPlayer::LoadedMedia; + + if (newStatus != m_mediaStatus) + Q_EMIT mediaStatusChanged((m_mediaStatus = newStatus)); + + } + + if (newState == QMediaPlayer::PlayingState && [static_cast<AVFMediaPlayerObserver*>(m_observer) player]) { + // Setting the rate is enough to start playback, no need to call play() + [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] setRate:m_rate]; + m_playbackTimer.start(); + } +} + + +void AVFMediaPlayer::processLoadStateChange() +{ + processLoadStateChange(m_state); +} + + +void AVFMediaPlayer::processLoadStateFailure() +{ + Q_EMIT stateChanged((m_state = QMediaPlayer::StoppedState)); +} + +void AVFMediaPlayer::processBufferStateChange(int bufferProgress) +{ + if (bufferProgress == m_bufferProgress) + return; + + auto status = m_mediaStatus; + // Buffered -> unbuffered. + if (!bufferProgress) { + status = QMediaPlayer::StalledMedia; + } else if (status == QMediaPlayer::StalledMedia) { + status = QMediaPlayer::BufferedMedia; + // Resume playback. + if (m_state == QMediaPlayer::PlayingState) { + [[static_cast<AVFMediaPlayerObserver*>(m_observer) player] setRate:m_rate]; + m_playbackTimer.start(); + } + } + + if (m_mediaStatus != status) + Q_EMIT mediaStatusChanged(m_mediaStatus = status); + + m_bufferProgress = bufferProgress; + Q_EMIT bufferProgressChanged(bufferProgress/100.); +} + +void AVFMediaPlayer::processDurationChange(qint64 duration) +{ + if (duration == m_duration) + return; + + m_duration = duration; + Q_EMIT durationChanged(duration); +} + +void AVFMediaPlayer::processPositionChange() +{ + if (m_state == QMediaPlayer::StoppedState) + return; + + Q_EMIT positionChanged(position()); +} + +void AVFMediaPlayer::processMediaLoadError() +{ + if (m_requestedPosition != -1) { + m_requestedPosition = -1; + Q_EMIT positionChanged(position()); + } + + Q_EMIT mediaStatusChanged((m_mediaStatus = QMediaPlayer::InvalidMedia)); + + Q_EMIT error(QMediaPlayer::FormatError, tr("Failed to load media")); +} + +void AVFMediaPlayer::streamReady() +{ + setStreamURL(m_observer, m_resources.toEncoded()); +} + +void AVFMediaPlayer::streamDestroyed() +{ + resetStream(nullptr); +} + +void AVFMediaPlayer::updateTracks() +{ + bool firstLoad = true; + for (int i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i) { + if (tracks[i].count()) + firstLoad = false; + tracks[i].clear(); + nativeTracks[i].clear(); + } + AVPlayerItem *playerItem = [m_observer playerItem]; + if (playerItem) { + // Check each track for audio and video content + NSArray *tracks = playerItem.tracks; + for (AVPlayerItemTrack *track in tracks) { + AVAssetTrack *assetTrack = track.assetTrack; + if (assetTrack) { + int qtTrack = -1; + if ([assetTrack.mediaType isEqualToString:AVMediaTypeAudio]) { + qtTrack = QPlatformMediaPlayer::AudioStream; + setAudioAvailable(true); + } else if ([assetTrack.mediaType isEqualToString:AVMediaTypeVideo]) { + qtTrack = QPlatformMediaPlayer::VideoStream; + setVideoAvailable(true); + if (m_observer.videoTrack != track) { + m_observer.videoTrack = track; + bool isMirrored = false; + QtVideo::Rotation orientation = QtVideo::Rotation::None; + videoOrientationForAssetTrack(assetTrack, orientation, isMirrored); + orientationChanged(orientation, isMirrored); + } + } + else if ([assetTrack.mediaType isEqualToString:AVMediaTypeSubtitle]) { + qtTrack = QPlatformMediaPlayer::SubtitleStream; + } + if (qtTrack != -1) { + QMediaMetaData metaData = AVFMetaData::fromAssetTrack(assetTrack); + this->tracks[qtTrack].append(metaData); + nativeTracks[qtTrack].append(track); + } + } + } + // subtitles are disabled by default + if (firstLoad) + setActiveTrack(SubtitleStream, -1); + } + Q_EMIT tracksChanged(); +} + +void AVFMediaPlayer::setActiveTrack(QPlatformMediaPlayer::TrackType type, int index) +{ + const auto &t = nativeTracks[type]; + if (type == QPlatformMediaPlayer::SubtitleStream) { + // subtitle streams are not always automatically enabled on macOS/iOS. + // this hack ensures they get enables and we actually get the text + AVPlayerItem *playerItem = m_observer.m_playerItem; + if (playerItem) { + AVAsset *asset = playerItem.asset; + if (!asset) + return; + AVMediaSelectionGroup *group = [asset mediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible]; + if (!group) + return; + auto *options = group.options; + if (options.count) + [playerItem selectMediaOption:options.firstObject inMediaSelectionGroup:group]; + } + } + for (int i = 0; i < t.count(); ++i) + t.at(i).enabled = (i == index); + emit activeTracksChanged(); +} + +int AVFMediaPlayer::activeTrack(QPlatformMediaPlayer::TrackType type) +{ + const auto &t = nativeTracks[type]; + for (int i = 0; i < t.count(); ++i) + if (t.at(i).enabled) + return i; + return -1; +} + +int AVFMediaPlayer::trackCount(QPlatformMediaPlayer::TrackType type) +{ + return nativeTracks[type].count(); +} + +QMediaMetaData AVFMediaPlayer::trackMetaData(QPlatformMediaPlayer::TrackType type, int trackNumber) +{ + const auto &t = tracks[type]; + if (trackNumber < 0 || trackNumber >= t.count()) + return QMediaMetaData(); + return t.at(trackNumber); +} + +void AVFMediaPlayer::resetStream(QIODevice *stream) +{ + if (m_mediaStream) { + disconnect(m_mediaStream, &QIODevice::readyRead, this, &AVFMediaPlayer::streamReady); + disconnect(m_mediaStream, &QIODevice::destroyed, this, &AVFMediaPlayer::streamDestroyed); + } + + m_mediaStream = stream; + + if (m_mediaStream) { + connect(m_mediaStream, &QIODevice::readyRead, this, &AVFMediaPlayer::streamReady); + connect(m_mediaStream, &QIODevice::destroyed, this, &AVFMediaPlayer::streamDestroyed); + } +} + +void AVFMediaPlayer::nativeSizeChanged(QSize size) +{ + if (!m_videoSink) + return; + m_videoSink->setNativeSize(size); +} + +void AVFMediaPlayer::orientationChanged(QtVideo::Rotation rotation, bool mirrored) +{ + if (!m_videoOutput) + return; + + m_videoOutput->setVideoRotation(rotation); + m_videoOutput->setVideoMirrored(mirrored); +} + +void AVFMediaPlayer::videoOrientationForAssetTrack(AVAssetTrack *videoTrack, + QtVideo::Rotation &angle, + bool &mirrored) +{ + angle = QtVideo::Rotation::None; + if (videoTrack) { + CGAffineTransform transform = videoTrack.preferredTransform; + if (CGAffineTransformIsIdentity(transform)) + return; + qreal delta = transform.a * transform.d - transform.b * transform.c; + qreal radians = qAtan2(transform.b, transform.a); + qreal degrees = qRadiansToDegrees(radians); + qreal scaleX = (transform.a/qAbs(transform.a)) * qSqrt(qPow(transform.a, 2) + qPow(transform.c, 2)); + qreal scaleY = (transform.d/abs(transform.d)) * qSqrt(qPow(transform.b, 2) + qPow(transform.d, 2)); + + if (delta < 0.0) { // flipped + if (scaleX < 0.0) { + // vertical flip + degrees = -degrees; + } else if (scaleY < 0.0) { + // horizontal flip + degrees = (180 + (int)degrees) % 360; + } + mirrored = true; + } + + if (qFuzzyCompare(degrees, qreal(90)) || qFuzzyCompare(degrees, qreal(-270))) { + angle = QtVideo::Rotation::Clockwise90; + } else if (qFuzzyCompare(degrees, qreal(-90)) || qFuzzyCompare(degrees, qreal(270))) { + angle = QtVideo::Rotation::Clockwise270; + } else if (qFuzzyCompare(degrees, qreal(180)) || qFuzzyCompare(degrees, qreal(-180))) { + angle = QtVideo::Rotation::Clockwise180; + } + } +} + +#include "moc_avfmediaplayer_p.cpp" |