Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
avfmediaplayer.mm
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd and/or its subsidiary(-ies).
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
5
6#include <avfvideosink_p.h>
7#include <common/avfmetadata_p.h>
8#include <mediaplayer/avfvideorenderercontrol_p.h>
9
10#include <QtMultimedia/qaudiooutput.h>
11#include <QtMultimedia/private/qplatformaudiooutput_p.h>
12#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) || defined(Q_OS_TVOS)
13# include <QtMultimedia/private/qavaudiosessionmanager_p.h>
14#endif
15
16#include <QtCore/qdir.h>
17#include <QtCore/qfileinfo.h>
18#include <QtCore/qlocale.h>
19#include <QtCore/qmath.h>
20#include <QtCore/qmimedatabase.h>
21#include <QtCore/qmutex.h>
22#include <QtCore/qpointer.h>
23#include <QtCore/qthread.h>
24#include <QtCore/private/qexpected_p.h>
25
26#include <memory>
27#include <optional>
28
29#import <AVFoundation/AVFoundation.h>
30
31QT_USE_NAMESPACE
32
33//AVAsset Keys
34static NSString* const AVF_TRACKS_KEY = @"tracks";
35static NSString* const AVF_PLAYABLE_KEY = @"playable";
36
37//AVPlayerItem keys
38static NSString* const AVF_STATUS_KEY = @"status";
39static NSString* const AVF_BUFFER_LIKELY_KEEP_UP_KEY = @"playbackLikelyToKeepUp";
40
41//AVPlayer keys
42static NSString* const AVF_RATE_KEY = @"rate";
43static NSString* const AVF_CURRENT_ITEM_KEY = @"currentItem";
44static NSString* const AVF_CURRENT_ITEM_DURATION_KEY = @"currentItem.duration";
45
54
55@interface AVFMediaPlayerObserver : NSObject<AVAssetResourceLoaderDelegate>
56
57@property (readonly, getter=player) AVPlayer* m_player;
58@property (readonly, getter=playerItem) AVPlayerItem* m_playerItem;
59@property (readonly, getter=playerLayer) AVPlayerLayer* m_playerLayer;
60@property (retain) AVPlayerItemTrack *videoTrack;
61
62- (AVFMediaPlayerObserver *) initWithMediaPlayerSession:(AVFMediaPlayer *)session;
63- (void) setURL:(NSURL *)url mimeType:(NSString *)mimeType;
64- (void) unloadMedia;
65- (void) prepareToPlayAsset:(AVURLAsset *)asset withKeys:(NSArray *)requestedKeys;
66- (void) assetFailedToPrepareForPlayback:(NSError *)error;
67- (void)loadSubtitleOptionsForAsset:(AVAsset *)asset generation:(int)generation;
68- (void) playerItemDidReachEnd:(NSNotification *)notification;
69- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
70 change:(NSDictionary *)change context:(void *)context;
71- (void) clearSession;
72- (void) notifySeekComplete;
73- (void) dealloc;
74- (BOOL) resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
75@end
76
77namespace {
78
79struct GuardedPlatformPlayer
80{
81 mutable QMutex mutex;
82 AVFMediaPlayer *player{};
83
84 explicit operator bool() const
85 {
86 std::lock_guard guard(mutex);
87 return player;
88 }
89
90 struct not_a_platform_player_t
91 {
92 };
93
94 template <typename Functor>
95 auto withPlatformPlayer(Functor &&f)
96 -> q23::expected<std::invoke_result_t<Functor, AVFMediaPlayer *>,
97 not_a_platform_player_t>
98 {
99 std::unique_lock guard(mutex);
100 if (!player)
101 return q23::unexpected{ not_a_platform_player_t{} };
102 if constexpr (std::is_void_v<std::invoke_result_t<Functor, AVFMediaPlayer *>>) {
103 f(player);
104 return {};
105 } else {
106 return f(player);
107 }
108 }
109
110 template <typename Functor>
111 void invokeWithPlatformPlayer(Functor f)
112 {
113 std::unique_lock guard(mutex);
114 if (!player)
115 return;
116
117 if (player->thread()->isCurrentThread()) {
118 guard.unlock();
119 f(player);
120 } else {
121 QMetaObject::invokeMethod(player, [f = std::move(f), player = player]() {
122 f(player);
123 });
124 }
125 }
126
127 void clear()
128 {
129 std::lock_guard<QMutex> guard(mutex);
130 player = nullptr;
131 }
132};
133} // namespace
134
135#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) || defined(Q_OS_TVOS)
136using ActivationToken = QtMultimediaPrivate::QAVAudioSessionManager::ActivationToken;
137#endif
138
139namespace {
140
141// OS-generated subtitles are legible options with this characteristic
142bool isMachineGenerated([[maybe_unused]] AVMediaSelectionOption *option)
143{
144#if QT_APPLE_SDK_EQUAL_OR_ABOVE(MACOS(26), IOS(26))
145 if (@available(macOS 26.0, iOS 26.0, *))
146 return [option hasMediaCharacteristic:AVMediaCharacteristicMachineGenerated];
147#endif
148 return false;
149}
150
151template <typename T>
152std::shared_ptr<T> retainObject(T *object)
153{
154 return std::shared_ptr<T>((T *)[object retain], [](T *p) {
155 [p release];
156 });
157}
158
159QMediaMetaData subtitleMetaData(AVMediaSelectionOption *option)
160{
161 QMediaMetaData metaData;
162 metaData.insert(QMediaMetaData::Title, QString::fromNSString(option.displayName));
163 if (option.extendedLanguageTag) {
164 const QLocale::Language language =
165 QLocale::codeToLanguage(QString::fromNSString(option.extendedLanguageTag));
166 if (language != QLocale::AnyLanguage)
167 metaData.insert(QMediaMetaData::Language, language);
168 }
169 return metaData;
170}
171} // namespace
172
173@implementation AVFMediaPlayerObserver {
174@private
175 GuardedPlatformPlayer m_platformPlayer;
176 AVPlayer *m_player;
177 AVPlayerItem *m_playerItem;
178 AVPlayerLayer *m_playerLayer;
179 NSURL *m_URL;
180 BOOL m_bufferIsLikelyToKeepUp;
181 NSData *m_data;
182 NSString *m_mimeType;
183#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) || defined(Q_OS_TVOS)
184 std::optional<ActivationToken> m_sessionActivation;
185#endif
186}
187
188@synthesize m_player, m_playerItem, m_playerLayer;
189
190- (AVFMediaPlayerObserver *) initWithMediaPlayerSession:(AVFMediaPlayer *)session
191{
192 if (!(self = [super init]))
193 return nil;
194 m_platformPlayer.player = session;
195 m_bufferIsLikelyToKeepUp = FALSE;
196
197 m_playerLayer = [AVPlayerLayer playerLayerWithPlayer:nil];
198 [m_playerLayer retain];
199 m_playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
200 m_playerLayer.anchorPoint = CGPointMake(0.0f, 0.0f);
201 return self;
202}
203
204- (void) setURL:(NSURL *)url mimeType:(NSString *)mimeType
205{
206 [m_mimeType release];
207 m_mimeType = [mimeType retain];
208
209 if (m_URL != url)
210 {
211 [m_URL release];
212 m_URL = [url copy];
213
214 //Create an asset for inspection of a resource referenced by a given URL.
215 //Load the values for the asset keys "tracks", "playable".
216
217 // use __block to avoid maintaining strong references on variables captured by the
218 // following block callback
219 __block BOOL isAccessing = [m_URL startAccessingSecurityScopedResource];
220 __block AVURLAsset *asset = [[AVURLAsset URLAssetWithURL:m_URL options:nil] retain];
221 [asset.resourceLoader setDelegate:self queue:dispatch_get_main_queue()];
222
223 __block NSArray *requestedKeys = [@[ AVF_TRACKS_KEY, AVF_PLAYABLE_KEY ] retain];
224
225 __block AVFMediaPlayerObserver *blockSelf = [self retain];
226
227 // Tells the asset to load the values of any of the specified keys that are not already loaded.
228 [asset loadValuesAsynchronouslyForKeys:requestedKeys
229 completionHandler:^{
230 dispatch_async(dispatch_get_main_queue(), ^{
231 if (isAccessing)
232 [m_URL stopAccessingSecurityScopedResource];
233 [blockSelf prepareToPlayAsset:asset withKeys:requestedKeys];
234 [asset release];
235 [requestedKeys release];
236 [blockSelf release];
237 });
238 }];
239 }
240}
241
242- (void) unloadMedia
243{
244 if (m_playerItem) {
245 [m_playerItem removeObserver:self forKeyPath:@"presentationSize"];
246 [m_playerItem removeObserver:self forKeyPath:AVF_STATUS_KEY];
247 [m_playerItem removeObserver:self forKeyPath:AVF_BUFFER_LIKELY_KEEP_UP_KEY];
248 [m_playerItem removeObserver:self forKeyPath:AVF_TRACKS_KEY];
249
250 [[NSNotificationCenter defaultCenter] removeObserver:self
251 name:AVPlayerItemDidPlayToEndTimeNotification
252 object:m_playerItem];
253 m_playerItem = nullptr;
254 }
255 if (m_player) {
256 [m_player setRate:0.0];
257 [m_player removeObserver:self forKeyPath:AVF_CURRENT_ITEM_DURATION_KEY];
258 [m_player removeObserver:self forKeyPath:AVF_CURRENT_ITEM_KEY];
259 [m_player removeObserver:self forKeyPath:AVF_RATE_KEY];
260 [m_player replaceCurrentItemWithPlayerItem:nil];
261
262 // Defer the release of AVPlayer to allow CoreMedia/VideoToolbox
263 // dispatch queues to finish pending operations. Releasing the
264 // player synchronously can cause sporadic crashes on macOS 14
265 // when background threads still reference internal resources.
266 AVPlayer *player = m_player;
267 m_player = nullptr;
268 dispatch_async(dispatch_get_main_queue(), ^{
269 [player release];
270 });
271 }
272 if (m_playerLayer)
273 m_playerLayer.player = nil;
274#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) || defined(Q_OS_TVOS)
275 m_sessionActivation.reset();
276#endif
277}
278
279- (void) prepareToPlayAsset:(AVURLAsset *)asset
280 withKeys:(NSArray *)requestedKeys
281{
282 if (!m_platformPlayer)
283 return;
284
285#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) || defined(Q_OS_TVOS)
286 auto sessionActivation = QtMultimediaPrivate::QAVAudioSessionManager::instance()->activate({
287 false,
288 AVAudioSessionCategoryOptionMixWithOthers,
289 });
290#endif
291
292 //Make sure that the value of each key has loaded successfully.
293 for (NSString *thisKey in requestedKeys)
294 {
295 NSError *error = nil;
296 AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error];
297#ifdef QT_DEBUG_AVF
298 qDebug() << Q_FUNC_INFO << [thisKey UTF8String] << " status: " << keyStatus;
299#endif
300 if (keyStatus == AVKeyValueStatusFailed)
301 {
302 [self assetFailedToPrepareForPlayback:error];
303 return;
304 }
305 }
306
307 //Use the AVAsset playable property to detect whether the asset can be played.
308#ifdef QT_DEBUG_AVF
309 qDebug() << Q_FUNC_INFO << "isPlayable: " << [asset isPlayable];
310#endif
311 if (!asset.playable)
312 qWarning() << "Asset reported to be not playable. Playback of this asset may not be possible.";
313
314 //At this point we're ready to set up for playback of the asset.
315 //Stop observing our prior AVPlayerItem, if we have one.
316 if (m_playerItem)
317 {
318 //Remove existing player item key value observers and notifications.
319 [self unloadMedia];
320 }
321
322 //Create a new instance of AVPlayerItem from the now successfully loaded AVAsset.
323 m_playerItem = [AVPlayerItem playerItemWithAsset:asset];
324 if (!m_playerItem) {
325 qWarning() << "Failed to create player item";
326 //Generate an error describing the failure.
327 NSString *localizedDescription = NSLocalizedString(@"Item cannot be played", @"Item cannot be played description");
328 NSString *localizedFailureReason = NSLocalizedString(@"The assets tracks were loaded, but couldn't create player item.", @"Item cannot be played failure reason");
329 NSDictionary *errorDict = @{
330 NSLocalizedDescriptionKey : localizedDescription,
331 NSLocalizedFailureReasonErrorKey : localizedFailureReason,
332 };
333 NSError *assetCannotBePlayedError = [NSError errorWithDomain:@"StitchedStreamPlayer" code:0 userInfo:errorDict];
334
335 [self assetFailedToPrepareForPlayback:assetCannotBePlayedError];
336 return;
337 }
338
339 //Observe the player item "status" key to determine when it is ready to play.
340 [m_playerItem addObserver:self
341 forKeyPath:AVF_STATUS_KEY
342 options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
343 context:AVFMediaPlayerObserverStatusObservationContext];
344
345 [m_playerItem addObserver:self
346 forKeyPath:@"presentationSize"
347 options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
348 context:AVFMediaPlayerObserverPresentationSizeContext];
349
350 [m_playerItem addObserver:self
351 forKeyPath:AVF_BUFFER_LIKELY_KEEP_UP_KEY
352 options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
353 context:AVFMediaPlayerObserverBufferLikelyToKeepUpContext];
354
355 [m_playerItem addObserver:self
356 forKeyPath:AVF_TRACKS_KEY
357 options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
358 context:AVFMediaPlayerObserverTracksContext];
359
360 //When the player item has played to its end time we'll toggle
361 //the movie controller Pause button to be the Play button
362 [[NSNotificationCenter defaultCenter] addObserver:self
363 selector:@selector(playerItemDidReachEnd:)
364 name:AVPlayerItemDidPlayToEndTimeNotification
365 object:m_playerItem];
366
367 //Get a new AVPlayer initialized to play the specified player item.
368 m_player = [AVPlayer playerWithPlayerItem:m_playerItem];
369 [m_player retain];
370
371 //Set the initial audio ouptut settings on new player object
372 {
373 m_platformPlayer.withPlatformPlayer([&](AVFMediaPlayer *player) {
374 auto *audioOutput = player->m_audioOutput;
375 m_player.volume = (audioOutput ? audioOutput->volume : 1.);
376 m_player.muted = (audioOutput ? audioOutput->muted : true);
377 player->updateAudioOutputDevice();
378 });
379 }
380
381 //Assign the output layer to the new player
382 m_playerLayer.player = m_player;
383
384 //Observe the AVPlayer "currentItem" property to find out when any
385 //AVPlayer replaceCurrentItemWithPlayerItem: replacement will/did
386 //occur.
387 [m_player addObserver:self
388 forKeyPath:AVF_CURRENT_ITEM_KEY
389 options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
390 context:AVFMediaPlayerObserverCurrentItemObservationContext];
391
392 //Observe the AVPlayer "rate" property to update the scrubber control.
393 [m_player addObserver:self
394 forKeyPath:AVF_RATE_KEY
395 options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
396 context:AVFMediaPlayerObserverRateObservationContext];
397
398 //Observe the duration for getting the buffer state
399 [m_player addObserver:self
400 forKeyPath:AVF_CURRENT_ITEM_DURATION_KEY
401 options:0
402 context:AVFMediaPlayerObserverCurrentItemDurationObservationContext];
403#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) || defined(Q_OS_TVOS)
404 m_sessionActivation = std::move(sessionActivation);
405#endif
406}
407
408-(void) assetFailedToPrepareForPlayback:(NSError *)error
409{
410 QMediaPlayer::Error errorCode = QMediaPlayer::FormatError;
411 if (error) {
412 NSError *underlyingError = error.userInfo[NSUnderlyingErrorKey];
413 if (underlyingError && ![underlyingError.domain isEqualToString:AVFoundationErrorDomain])
414 errorCode = QMediaPlayer::ResourceError;
415 }
416 m_platformPlayer.invokeWithPlatformPlayer([errorCode](AVFMediaPlayer *platformPlayer) {
417 platformPlayer->processMediaLoadError(errorCode);
418 });
419
420#ifdef QT_DEBUG_AVF
421 qDebug() << Q_FUNC_INFO;
422 qDebug() << [[error localizedDescription] UTF8String];
423 qDebug() << [[error localizedFailureReason] UTF8String];
424 qDebug() << [[error localizedRecoverySuggestion] UTF8String];
425#endif
426}
427
428- (void)loadSubtitleOptionsForAsset:(AVAsset *)asset generation:(int)generation
429{
430 auto observer = retainObject(self);
431 [asset loadMediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible
432 completionHandler:[observer,
433 generation](AVMediaSelectionGroup *group,
434 NSError *) {
435 std::shared_ptr<AVMediaSelectionGroup> retained = retainObject(group);
436 observer->m_platformPlayer.invokeWithPlatformPlayer(
437 [generation, retained](AVFMediaPlayer *platformPlayer) {
438 platformPlayer->subtitleOptionsLoaded(generation, retained.get());
439 });
440 }];
441}
442
443- (void) playerItemDidReachEnd:(NSNotification *)notification
444{
445 Q_UNUSED(notification);
446
447 m_platformPlayer.invokeWithPlatformPlayer([](AVFMediaPlayer *platformPlayer) {
448 platformPlayer->processEOS();
449 });
450}
451
452- (void) observeValueForKeyPath:(NSString*) path
453 ofObject:(id)object
454 change:(NSDictionary*)change
455 context:(void*)context
456{
457 //AVPlayerItem "status" property value observer.
458 if (context == AVFMediaPlayerObserverStatusObservationContext)
459 {
460 AVPlayerStatus status = (AVPlayerStatus)[[change objectForKey:NSKeyValueChangeNewKey] integerValue];
461 switch (status)
462 {
463 //Indicates that the status of the player is not yet known because
464 //it has not tried to load new media resources for playback
465 case AVPlayerStatusUnknown:
466 {
467 //QMetaObject::invokeMethod(m_session, "processLoadStateChange", Qt::AutoConnection);
468 }
469 break;
470
471 case AVPlayerStatusReadyToPlay:
472 {
473 //Once the AVPlayerItem becomes ready to play, i.e.
474 //[playerItem status] == AVPlayerItemStatusReadyToPlay,
475 //its duration can be fetched from the item.
476
477 m_platformPlayer.invokeWithPlatformPlayer([](AVFMediaPlayer *platformPlayer) {
478 platformPlayer->processLoadStateChange();
479 });
480 }
481 break;
482
483 case AVPlayerStatusFailed:
484 {
485 AVPlayerItem *playerItem = static_cast<AVPlayerItem*>(object);
486 [self assetFailedToPrepareForPlayback:playerItem.error];
487
488 m_platformPlayer.invokeWithPlatformPlayer([](AVFMediaPlayer *platformPlayer) {
489 platformPlayer->processLoadStateChange();
490 });
491 }
492 break;
493 }
494 } else if (context == AVFMediaPlayerObserverPresentationSizeContext) {
495 QSize size(m_playerItem.presentationSize.width, m_playerItem.presentationSize.height);
496 m_platformPlayer.invokeWithPlatformPlayer([size](AVFMediaPlayer *platformPlayer) {
497 platformPlayer->nativeSizeChanged(size);
498 });
499 } else if (context == AVFMediaPlayerObserverBufferLikelyToKeepUpContext)
500 {
501 const bool isPlaybackLikelyToKeepUp = [m_playerItem isPlaybackLikelyToKeepUp];
502 if (isPlaybackLikelyToKeepUp != m_bufferIsLikelyToKeepUp) {
503 m_bufferIsLikelyToKeepUp = isPlaybackLikelyToKeepUp;
504 int bufferProgress = isPlaybackLikelyToKeepUp ? 100 : 0;
505
506 m_platformPlayer.invokeWithPlatformPlayer(
507 [bufferProgress](AVFMediaPlayer *platformPlayer) {
508 platformPlayer->processBufferStateChange(bufferProgress);
509 });
510 }
511 }
512 else if (context == AVFMediaPlayerObserverTracksContext)
513 {
514 m_platformPlayer.invokeWithPlatformPlayer([](AVFMediaPlayer *platformPlayer) {
515 platformPlayer->updateTracks();
516 });
517 }
518 //AVPlayer "rate" property value observer.
519 else if (context == AVFMediaPlayerObserverRateObservationContext) {
520 //QMetaObject::invokeMethod(m_session, "setPlaybackRate", Qt::AutoConnection, Q_ARG(qreal, [m_player rate]));
521 }
522 //AVPlayer "currentItem" property observer.
523 //Called when the AVPlayer replaceCurrentItemWithPlayerItem:
524 //replacement will/did occur.
525 else if (context == AVFMediaPlayerObserverCurrentItemObservationContext) {
526 AVPlayerItem *newPlayerItem = [change objectForKey:NSKeyValueChangeNewKey];
527 if (m_playerItem != newPlayerItem)
528 m_playerItem = newPlayerItem;
529 } else if (context == AVFMediaPlayerObserverCurrentItemDurationObservationContext) {
530 const CMTime time = [m_playerItem duration];
531 const qint64 dur = static_cast<qint64>(float(time.value) / float(time.timescale) * 1000.0f);
532
533 m_platformPlayer.invokeWithPlatformPlayer([dur](AVFMediaPlayer *platformPlayer) {
534 platformPlayer->processDurationChange(dur);
535 });
536 } else {
537 [super observeValueForKeyPath:path ofObject:object change:change context:context];
538 }
539}
540
541- (void)clearSession
542{
543#ifdef QT_DEBUG_AVF
544 qDebug() << Q_FUNC_INFO;
545#endif
546 m_platformPlayer.clear();
547}
548
549- (void)notifySeekComplete
550{
551 m_platformPlayer.withPlatformPlayer([](AVFMediaPlayer *player) {
552 player->seekCompleted();
553 });
554}
555
556- (void) dealloc
557{
558#ifdef QT_DEBUG_AVF
559 qDebug() << Q_FUNC_INFO;
560#endif
561 [self unloadMedia];
562
563 m_platformPlayer.clear();
564
565 if (m_URL) {
566 [m_URL release];
567 }
568
569 [m_mimeType release];
570 [m_playerLayer release];
571 // 'videoTrack' is a 'retain' property, but still needs a
572 // manual 'release' (i.e. setting to nil):
573 self.videoTrack = nil;
574 [super dealloc];
575}
576
577- (BOOL) resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
578{
579 Q_UNUSED(resourceLoader);
580
581 if (![loadingRequest.request.URL.scheme isEqualToString:@"iodevice"])
582 return NO;
583
584 auto result = m_platformPlayer.withPlatformPlayer([&](AVFMediaPlayer *platformPlayer) {
585 QIODevice *device = platformPlayer->mediaStream();
586 if (!device)
587 return NO;
588
589 device->seek(loadingRequest.dataRequest.requestedOffset);
590 if (loadingRequest.contentInformationRequest) {
591 loadingRequest.contentInformationRequest.contentType = m_mimeType;
592 loadingRequest.contentInformationRequest.contentLength = device->size();
593 loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
594 }
595
596 if (loadingRequest.dataRequest) {
597 NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
598 int maxBytes = qMin(32 * 1024, int(requestedLength));
599 QByteArray buffer;
600 buffer.resize(maxBytes);
601
602 NSInteger submitted = 0;
603 while (submitted < requestedLength) {
604 qint64 len = device->read(buffer.data(), maxBytes);
605 if (len < 1)
606 break;
607
608 [loadingRequest.dataRequest respondWithData:buffer.toNSData()];
609 submitted += len;
610 }
611
612 // Finish loading even if not all bytes submitted.
613 [loadingRequest finishLoading];
614 }
615
616 return YES;
617 });
618
619 return result.value_or(NO);
620}
621@end
622
623AVFMediaPlayer::AVFMediaPlayer(QMediaPlayer *player)
624 : QObject(player),
625 QPlatformMediaPlayer(player),
626 m_mediaStream(nullptr),
627 m_rate(1.0),
628 m_requestedPosition(-1),
629 m_duration(0),
630 m_bufferProgress(0)
631{
632 m_observer = [[AVFMediaPlayerObserver alloc] initWithMediaPlayerSession:this];
633 connect(&m_playbackTimer, &QTimer::timeout, this, &AVFMediaPlayer::processPositionChange);
634 setVideoOutput(new AVFVideoRendererControl(this));
635}
636
638{
639#ifdef QT_DEBUG_AVF
640 qDebug() << Q_FUNC_INFO;
641#endif
642
643 // Unload media before the C++ side is torn down, so that
644 // CoreMedia/VideoToolbox threads don't outlive our objects.
645 [m_observer unloadMedia];
646
647 //Detatch the session from the sessionObserver (which could still be alive trying to communicate with this session).
648 [m_observer clearSession];
649 [m_observer release];
650 [m_subtitleGroup release];
651}
652
653void AVFMediaPlayer::setVideoSink(QVideoSink *sink)
654{
655 m_videoSink = sink ? static_cast<AVFVideoSink *>(sink->platformVideoSink()): nullptr;
656 if (m_videoSink && m_nativeSize.isValid())
657 m_videoSink->setNativeSize(m_nativeSize);
658 m_videoOutput->setVideoSink(m_videoSink);
659}
660
662{
663#ifdef QT_DEBUG_AVF
664 qDebug() << Q_FUNC_INFO << output;
665#endif
666
667 if (m_videoOutput == output)
668 return;
669
670 //Set the current output layer to null to stop rendering
671 if (m_videoOutput) {
672 m_videoOutput->setLayer(nullptr);
673 }
674
675 m_videoOutput = output;
676
677 if (m_videoOutput && state() != QMediaPlayer::StoppedState)
678 m_videoOutput->setLayer([m_observer playerLayer]);
679}
680
682{
683#ifdef QT_DEBUG_AVF
684 qDebug() << Q_FUNC_INFO;
685#endif
686 AVAsset *currentAsset = [[m_observer playerItem] asset];
687 return currentAsset;
688}
689
691{
692 return m_resources;
693}
694
696{
697 return m_mediaStream;
698}
699
700static void setURL(AVFMediaPlayerObserver *observer, const QUrl &url, const QString &mimeType = QString())
701{
702 QUrl resolvedUrl = url;
703 // AVFoundation cannot handle file URLs with a relative path
704 if (url.isLocalFile() && !QDir::isAbsolutePath(url.path()))
705 resolvedUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteFilePath());
706 NSURL *nsurl = resolvedUrl.toNSURL();
707 [observer setURL:nsurl mimeType:[NSString stringWithUTF8String:mimeType.toLatin1().constData()]];
708}
709
710
711void AVFMediaPlayer::setMedia(const QUrl &content, QIODevice *stream)
712{
713#ifdef QT_DEBUG_AVF
714 qDebug() << Q_FUNC_INFO << content.request().url();
715#endif
716
717 [m_observer unloadMedia];
718 m_nativeSize = { };
719
720 m_resources = content;
721 resetStream(stream);
722
723 m_requestedPosition = -1;
724 orientationChanged(QtVideo::Rotation::None, false);
725 positionChanged(position());
726 if (m_duration != 0) {
727 m_duration = 0;
728 durationChanged(0);
729 }
730 if (!m_metaData.isEmpty()) {
731 m_metaData.clear();
732 metaDataChanged();
733 }
734 resetBufferProgress();
735 resetSubtitleGroup();
736 for (size_t i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i) {
737 tracks[i].clear();
738 nativeTracks[i].clear();
739 }
740 tracksChanged();
741
742 if (!m_mediaStream && content.isEmpty()) {
743 seekableChanged(false);
744 audioAvailableChanged(false);
745 videoAvailableChanged(false);
746
747 mediaStatusChanged(QMediaPlayer::NoMedia);
748 stateChanged(QMediaPlayer::StoppedState);
749
750 return;
751 }
752
753 mediaStatusChanged(QMediaPlayer::LoadingMedia);
754
755 if (m_mediaStream) {
756 // If there is a data, try to load it,
757 // otherwise wait for readyRead.
758 if (m_mediaStream->size())
760 } else {
761 //Load AVURLAsset
762 //initialize asset using content's URL
763 setURL(m_observer, m_resources);
764 }
765
766 stateChanged(QMediaPlayer::StoppedState);
767}
768
770{
771 AVPlayerItem *playerItem = [m_observer playerItem];
772
773 if (m_requestedPosition != -1)
774 return m_requestedPosition;
775
776 if (!playerItem)
777 return 0;
778
779 CMTime time = [playerItem currentTime];
780 return static_cast<quint64>(float(time.value) / float(time.timescale) * 1000.0f);
781}
782
784{
785#ifdef QT_DEBUG_AVF
786 qDebug() << Q_FUNC_INFO;
787#endif
788 return m_duration;
789}
790
792{
793#ifdef QT_DEBUG_AVF
794 qDebug() << Q_FUNC_INFO;
795#endif
796 return m_bufferProgress/100.;
797}
798
800{
801 AVPlayerItem *playerItem = [m_observer playerItem];
802
803 if (!playerItem)
804 return {};
805
806 if (state() == QMediaPlayer::StoppedState)
807 return {};
808
809 QMediaTimeRange timeRanges;
810
811 NSArray *ranges = [playerItem loadedTimeRanges];
812 for (NSValue *timeRange in ranges) {
813 CMTimeRange currentTimeRange = [timeRange CMTimeRangeValue];
814 qint64 startTime = qint64(float(currentTimeRange.start.value) / currentTimeRange.start.timescale * 1000.0);
815 timeRanges.addInterval(startTime, startTime + qint64(float(currentTimeRange.duration.value) / currentTimeRange.duration.timescale * 1000.0));
816 }
817 return timeRanges;
818}
819
821{
822 return m_rate;
823}
824
825void AVFMediaPlayer::setAudioOutput(QPlatformAudioOutput *output)
826{
827 if (m_audioOutput == output)
828 return;
829 if (m_audioOutput)
830 m_audioOutput->q->disconnect(this);
831 m_audioOutput = output;
832 if (m_audioOutput) {
833 connect(m_audioOutput->q, &QAudioOutput::deviceChanged, this, &AVFMediaPlayer::updateAudioOutputDevice);
834 connect(m_audioOutput->q, &QAudioOutput::volumeChanged, this, &AVFMediaPlayer::setVolume);
835 connect(m_audioOutput->q, &QAudioOutput::mutedChanged, this, &AVFMediaPlayer::setMuted);
836 //connect(m_audioOutput->q, &QAudioOutput::audioRoleChanged, this, &AVFMediaPlayer::setAudioRole);
837 }
839 setMuted(m_audioOutput ? m_audioOutput->muted : true);
840 setVolume(m_audioOutput ? m_audioOutput->volume : 1.);
841}
842
844{
845 return m_metaData;
846}
847
848void AVFMediaPlayer::setPlaybackRate(qreal rate)
849{
850#ifdef QT_DEBUG_AVF
851 qDebug() << Q_FUNC_INFO << rate;
852#endif
853
854 if (QtPrivate::fuzzyCompare(m_rate, rate))
855 return;
856
857 m_rate = rate;
858
859 AVPlayer *player = [m_observer player];
860 if (player && state() == QMediaPlayer::PlayingState)
861 [player setRate:m_rate];
862
863 playbackRateChanged(m_rate);
864}
865
867{
868#ifdef QT_DEBUG_AVF
869 qDebug() << Q_FUNC_INFO << pos;
870#endif
871
872 if (pos == position())
873 return;
874
875 AVPlayerItem *playerItem = [m_observer playerItem];
876 if (!playerItem) {
877 m_requestedPosition = pos;
878 positionChanged(m_requestedPosition);
879 return;
880 }
881
882 if (!isSeekable()) {
883 if (m_requestedPosition != -1) {
884 m_requestedPosition = -1;
885 positionChanged(position());
886 }
887 return;
888 }
889
890 pos = qMax(qint64(0), pos);
891 if (duration() > 0)
892 pos = qMin(pos, duration());
893 m_requestedPosition = pos;
894
895 const CMTime newTime = CMTimeMake(pos, 1000);
896 AVFMediaPlayerObserver *observer = m_observer;
897 [playerItem seekToTime:newTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero
898 completionHandler:^(BOOL finished) {
899 if (finished)
900 [observer notifySeekComplete];
901 }];
902
903 positionChanged(pos);
904
905 // Reset media status if the current status is EndOfMedia
906 if (mediaStatus() == QMediaPlayer::EndOfMedia) {
907 QMediaPlayer::MediaStatus newMediaStatus = (state() == QMediaPlayer::PausedState)
908 ? QMediaPlayer::BufferedMedia
909 : QMediaPlayer::LoadedMedia;
910 mediaStatusChanged(newMediaStatus);
911 }
912}
913
915{
916#ifdef QT_DEBUG_AVF
917 qDebug() << Q_FUNC_INFO << "currently: " << state();
918#endif
919
920 if (mediaStatus() == QMediaPlayer::NoMedia || mediaStatus() == QMediaPlayer::InvalidMedia)
921 return;
922
923 if (state() == QMediaPlayer::PlayingState)
924 return;
925
926 if (state() != QMediaPlayer::PausedState)
927 resetCurrentLoop();
928
929 if (m_videoOutput && m_videoSink)
930 m_videoOutput->setLayer([m_observer playerLayer]);
931
932 // Reset media status if the current status is EndOfMedia
933 if (mediaStatus() == QMediaPlayer::EndOfMedia)
934 setPosition(0);
935
936 if (mediaStatus() == QMediaPlayer::LoadedMedia
937 || mediaStatus() == QMediaPlayer::BufferedMedia) {
938 // Setting the rate starts playback
939 [[m_observer player] setRate:m_rate];
940 }
941
942 processLoadStateChange(QMediaPlayer::PlayingState);
943
944 stateChanged(QMediaPlayer::PlayingState);
945 m_playbackTimer.start(100);
946}
947
949{
950#ifdef QT_DEBUG_AVF
951 qDebug() << Q_FUNC_INFO << "currently: " << state();
952#endif
953
954 if (mediaStatus() == QMediaPlayer::NoMedia || mediaStatus() == QMediaPlayer::InvalidMedia)
955 return;
956
957 if (state() == QMediaPlayer::PausedState)
958 return;
959
960 stateChanged(QMediaPlayer::PausedState);
961
962 if (m_videoOutput && m_videoSink)
963 m_videoOutput->setLayer([m_observer playerLayer]);
964
965 [[m_observer player] pause];
966
967 // Reset media status if the current status is EndOfMedia
968 if (mediaStatus() == QMediaPlayer::EndOfMedia)
969 setPosition(0);
970
971 positionChanged(position());
972 m_playbackTimer.stop();
973}
974
976{
977#ifdef QT_DEBUG_AVF
978 qDebug() << Q_FUNC_INFO << "currently: " << state();
979#endif
980
981 if (state() == QMediaPlayer::StoppedState && mediaStatus() != QMediaPlayer::EndOfMedia)
982 return;
983
984 // AVPlayer doesn't have stop(), only pause() and play().
985 [[m_observer player] pause];
986 setPosition(0);
987
988 if (m_videoOutput)
989 m_videoOutput->setLayer(nullptr);
990
991 resetBufferProgress();
992
993 if (mediaStatus() == QMediaPlayer::BufferedMedia || mediaStatus() == QMediaPlayer::EndOfMedia)
994 mediaStatusChanged(QMediaPlayer::LoadedMedia);
995
996 stateChanged(QMediaPlayer::StoppedState);
997 m_playbackTimer.stop();
998}
999
1000void AVFMediaPlayer::setVolume(float volume)
1001{
1002#ifdef QT_DEBUG_AVF
1003 qDebug() << Q_FUNC_INFO << volume;
1004#endif
1005
1006 AVPlayer *player = [m_observer player];
1007 if (player)
1008 player.volume = volume;
1009}
1010
1011void AVFMediaPlayer::setMuted(bool muted)
1012{
1013#ifdef QT_DEBUG_AVF
1014 qDebug() << Q_FUNC_INFO << muted;
1015#endif
1016
1017 AVPlayer *player = [m_observer player];
1018 if (player)
1019 player.muted = muted;
1020}
1021
1023{
1024#ifdef Q_OS_MACOS
1025 AVPlayer *player = [m_observer player];
1026 if (!player)
1027 return;
1028
1029 if (!m_audioOutput || m_audioOutput->device.id().isEmpty()) {
1030 if (!m_audioOutput)
1031 player.muted = true;
1032 player.audioOutputDeviceUniqueID = nil;
1033 } else {
1034 NSString *str = QString::fromUtf8(m_audioOutput->device.id()).toNSString();
1035 player.audioOutputDeviceUniqueID = str;
1036 }
1037#endif
1038}
1039
1041{
1042 if (doLoop()) {
1043 positionChanged(duration());
1044 setPosition(0);
1045 [[m_observer player] setRate:m_rate];
1046 return;
1047 }
1048
1049 //AVPlayerItem has reached end of track/stream
1050#ifdef QT_DEBUG_AVF
1051 qDebug() << Q_FUNC_INFO;
1052#endif
1053 positionChanged(position());
1054
1055 if (m_videoOutput)
1056 m_videoOutput->setLayer(nullptr);
1057
1058 resetBufferProgress();
1059
1060 stateChanged(QMediaPlayer::StoppedState);
1061 mediaStatusChanged(QMediaPlayer::EndOfMedia);
1062}
1063
1064void AVFMediaPlayer::processLoadStateChange(QMediaPlayer::PlaybackState newState)
1065{
1066 AVPlayerStatus currentStatus = [[m_observer player] status];
1067
1068#ifdef QT_DEBUG_AVF
1069 qDebug() << Q_FUNC_INFO << currentStatus << ", " << mediaStatus() << ", " << newState;
1070#endif
1071
1072 if (mediaStatus() == QMediaPlayer::NoMedia)
1073 return;
1074
1075 if (currentStatus == AVPlayerStatusReadyToPlay) {
1076
1077 AVPlayerItem *playerItem = [m_observer playerItem];
1078
1079 applyPitchCompensation(m_pitchCompensationEnabled);
1080
1081 // get the meta data
1082 m_metaData = AVFMetaData::fromAsset(playerItem.asset);
1083 metaDataChanged();
1085
1086 if (playerItem) {
1087 seekableChanged([[playerItem seekableTimeRanges] count] > 0);
1088
1089 // Get the native size of the video, and reset the bounds of the player layer
1090 AVPlayerLayer *playerLayer = [m_observer playerLayer];
1091 if (m_observer.videoTrack && playerLayer) {
1092 if (!playerLayer.bounds.size.width || !playerLayer.bounds.size.height) {
1093 playerLayer.bounds = CGRectMake(0.0f, 0.0f,
1094 m_observer.videoTrack.assetTrack.naturalSize.width,
1095 m_observer.videoTrack.assetTrack.naturalSize.height);
1096 }
1097 }
1098
1099 if (m_requestedPosition != -1)
1100 setPosition(m_requestedPosition);
1101 }
1102
1103 QMediaPlayer::MediaStatus newStatus = (newState != QMediaPlayer::StoppedState)
1104 ? QMediaPlayer::BufferedMedia
1105 : QMediaPlayer::LoadedMedia;
1106
1107 if (newStatus != mediaStatus()) {
1108 if (newStatus == QMediaPlayer::BufferedMedia
1109 && mediaStatus() == QMediaPlayer::LoadingMedia) {
1110 // Emit intermediate transitions to match expected signal sequence
1111 mediaStatusChanged(QMediaPlayer::LoadedMedia);
1112 mediaStatusChanged(QMediaPlayer::BufferingMedia);
1113 } else if (newStatus == QMediaPlayer::BufferedMedia
1114 && mediaStatus() == QMediaPlayer::LoadedMedia) {
1115 mediaStatusChanged(QMediaPlayer::BufferingMedia);
1116 }
1117 mediaStatusChanged(newStatus);
1118 }
1119 }
1120
1121 if (newState == QMediaPlayer::PlayingState && [m_observer player]) {
1122 // Setting the rate is enough to start playback, no need to call play()
1123 [[m_observer player] setRate:m_rate];
1124 m_playbackTimer.start();
1125 }
1126}
1127
1128
1133
1134
1136{
1137 stateChanged(QMediaPlayer::StoppedState);
1138}
1139
1141{
1142 if (state() == QMediaPlayer::StoppedState)
1143 return;
1144
1145 if (bufferProgress == m_bufferProgress)
1146 return;
1147
1148 auto status = mediaStatus();
1149 // Buffered -> unbuffered.
1150 if (!bufferProgress) {
1151 status = QMediaPlayer::StalledMedia;
1152 } else if (status == QMediaPlayer::StalledMedia) {
1153 status = QMediaPlayer::BufferedMedia;
1154 // Resume playback.
1155 if (state() == QMediaPlayer::PlayingState) {
1156 [[m_observer player] setRate:m_rate];
1157 m_playbackTimer.start();
1158 }
1159 }
1160
1161 mediaStatusChanged(status);
1162
1163 m_bufferProgress = bufferProgress;
1164 bufferProgressChanged(bufferProgress / 100.);
1165}
1166
1168{
1169 if (duration == m_duration)
1170 return;
1171
1172 m_duration = duration;
1173 durationChanged(duration);
1174}
1175
1177{
1178 if (state() == QMediaPlayer::StoppedState)
1179 return;
1180
1181 positionChanged(position());
1182}
1183
1184void AVFMediaPlayer::processMediaLoadError(QMediaPlayer::Error errorCode)
1185{
1186 if (m_requestedPosition != -1) {
1187 m_requestedPosition = -1;
1188 positionChanged(position());
1189 }
1190
1191 setInvalidMediaWithError(errorCode, tr("Failed to load media"));
1192}
1193
1195{
1196 m_requestedPosition = -1;
1197}
1198
1203
1205{
1206 QString suffix;
1207 if (!m_resources.isEmpty())
1208 suffix = QFileInfo(m_resources.path()).suffix();
1209 if (suffix.isEmpty() && m_mediaStream)
1210 suffix = QMimeDatabase().mimeTypeForData(m_mediaStream).preferredSuffix();
1211 const QString url = QStringLiteral("iodevice:///iodevice.") + suffix;
1212 setURL(m_observer, QUrl(url), suffix);
1213}
1214
1216{
1217 resetStream(nullptr);
1218}
1219
1221{
1222 resetSubtitleGroup();
1223 bool firstLoad = true;
1224 for (size_t i = 0; i < QPlatformMediaPlayer::NTrackTypes; ++i) {
1225 if (tracks[i].count())
1226 firstLoad = false;
1227 tracks[i].clear();
1228 nativeTracks[i].clear();
1229 }
1230 bool hasAudio = false;
1231 bool hasVideo = false;
1232 AVPlayerItem *playerItem = [m_observer playerItem];
1233 if (playerItem) {
1234 // Check each track for audio and video content
1235 NSArray *tracks = playerItem.tracks;
1236 for (AVPlayerItemTrack *track in tracks) {
1237 AVAssetTrack *assetTrack = track.assetTrack;
1238 if (assetTrack) {
1239 std::optional<TrackType> qtTrack;
1240 if ([assetTrack.mediaType isEqualToString:AVMediaTypeAudio]) {
1241 qtTrack = TrackType::AudioStream;
1242 hasAudio = true;
1243 } else if ([assetTrack.mediaType isEqualToString:AVMediaTypeVideo]) {
1244 qtTrack = TrackType::VideoStream;
1245 hasVideo = true;
1246 if (m_observer.videoTrack != track) {
1247 m_observer.videoTrack = track;
1248 bool isMirrored = false;
1249 QtVideo::Rotation orientation = QtVideo::Rotation::None;
1250 videoOrientationForAssetTrack(assetTrack, orientation, isMirrored);
1251 orientationChanged(orientation, isMirrored);
1252 }
1253 }
1254 else if ([assetTrack.mediaType isEqualToString:AVMediaTypeSubtitle]) {
1255 qtTrack = TrackType::SubtitleStream;
1256 }
1257 if (qtTrack) {
1258 QMediaMetaData metaData = AVFMetaData::fromAssetTrack(assetTrack);
1259 this->tracks[*qtTrack].append(metaData);
1260 nativeTracks[*qtTrack].append(track);
1261 }
1262 }
1263 }
1264 if (@available(macOS 26.0, iOS 26.0, *))
1265 [m_observer loadSubtitleOptionsForAsset:playerItem.asset
1266 generation:m_subtitleGroupGeneration];
1267 // subtitles are disabled by default
1268 if (firstLoad)
1269 setActiveTrack(TrackType::SubtitleStream, -1);
1270 }
1271 audioAvailableChanged(hasAudio);
1272 videoAvailableChanged(hasVideo);
1273 tracksChanged();
1274}
1275
1276void AVFMediaPlayer::setActiveTrack(TrackType type, int index)
1277{
1278 if (type == TrackType::SubtitleStream && m_subtitleGroup) {
1279 m_requestedSubtitleTrack = index;
1280 applySubtitleSelection();
1281 activeTracksChanged();
1282 return;
1283 }
1284
1285 const auto &t = nativeTracks[type];
1286 if (type == TrackType::SubtitleStream) {
1287 // subtitle streams are not always automatically enabled on macOS/iOS.
1288 // this hack ensures they get enables and we actually get the text
1289 AVPlayerItem *playerItem = m_observer.m_playerItem;
1290 if (playerItem) {
1291 AVAsset *asset = playerItem.asset;
1292 if (!asset)
1293 return;
1294 [asset loadMediaSelectionGroupForMediaCharacteristic:AVMediaCharacteristicLegible
1295 completionHandler:[=](AVMediaSelectionGroup *group,
1296 NSError *error) {
1297 if (error)
1298 return; // FIXME: handle error
1299 auto *options = group.options;
1300 // An explicit selection overrides the OS automatic selection, which can
1301 // enable OS-generated subtitles. Deselect for index -1.
1302 AVMediaSelectionOption *option =
1303 index >= 0 && options.count ? options.firstObject : nil;
1304 if (!option && !group.allowsEmptySelection)
1305 return;
1306 [playerItem selectMediaOption:option inMediaSelectionGroup:group];
1307 }];
1308 }
1309 }
1310 for (int i = 0; i < t.count(); ++i)
1311 t.at(i).enabled = (i == index);
1312 activeTracksChanged();
1313}
1314
1315void AVFMediaPlayer::subtitleOptionsLoaded(int generation, AVMediaSelectionGroup *group)
1316{
1317 if (generation != m_subtitleGroupGeneration || !group)
1318 return;
1319
1320 bool hasGeneratedSubtitles = false;
1321 for (AVMediaSelectionOption *option in group.options)
1322 hasGeneratedSubtitles |= isMachineGenerated(option);
1323 if (!hasGeneratedSubtitles)
1324 return;
1325
1326 m_subtitleGroup = [group retain];
1327
1328 QList<QMediaMetaData> subtitles;
1329 for (AVMediaSelectionOption *option in group.options)
1330 subtitles.append(subtitleMetaData(option));
1331 tracks[TrackType::SubtitleStream] = std::move(subtitles);
1332
1333 applySubtitleSelection();
1334 tracksChanged();
1335 activeTracksChanged();
1336}
1337
1338void AVFMediaPlayer::resetSubtitleGroup()
1339{
1340 ++m_subtitleGroupGeneration;
1341 [m_subtitleGroup release];
1342 m_subtitleGroup = nil;
1343}
1344
1345void AVFMediaPlayer::applySubtitleSelection()
1346{
1347 AVPlayerItem *playerItem = [m_observer playerItem];
1348 if (!playerItem || !m_subtitleGroup)
1349 return;
1350
1351 // An explicit selection overrides the automatic selection of AVFoundation, which
1352 // would otherwise enable the OS-generated subtitles. Index -1 deselects.
1353 NSArray<AVMediaSelectionOption *> *options = m_subtitleGroup.options;
1354 const int index = m_requestedSubtitleTrack;
1355 AVMediaSelectionOption *option =
1356 index >= 0 && index < int(options.count) ? options[index] : nil;
1357 if (!option && !m_subtitleGroup.allowsEmptySelection)
1358 return;
1359 [playerItem selectMediaOption:option inMediaSelectionGroup:m_subtitleGroup];
1360}
1361
1362int AVFMediaPlayer::activeTrack(TrackType type)
1363{
1364 if (type == TrackType::SubtitleStream && m_subtitleGroup) {
1365 AVPlayerItem *playerItem = [m_observer playerItem];
1366 if (!playerItem)
1367 return -1;
1368 AVMediaSelectionOption *selected = [playerItem.currentMediaSelection
1369 selectedMediaOptionInMediaSelectionGroup:m_subtitleGroup];
1370 const NSUInteger index = [m_subtitleGroup.options indexOfObject:selected];
1371 return index == NSNotFound ? -1 : int(index);
1372 }
1373
1374 const auto &t = nativeTracks[type];
1375 for (int i = 0; i < t.count(); ++i)
1376 if (t.at(i).enabled)
1377 return i;
1378 return -1;
1379}
1380
1381int AVFMediaPlayer::trackCount(TrackType type)
1382{
1383 if (type == TrackType::SubtitleStream && m_subtitleGroup)
1384 return tracks[type].count();
1385 return nativeTracks[type].count();
1386}
1387
1388QMediaMetaData AVFMediaPlayer::trackMetaData(TrackType type, int trackNumber)
1389{
1390 const auto &t = tracks[type];
1391 if (trackNumber < 0 || trackNumber >= t.count())
1392 return QMediaMetaData();
1393 return t.at(trackNumber);
1394}
1395
1396void AVFMediaPlayer::resetStream(QIODevice *stream)
1397{
1398 if (m_mediaStream) {
1399 disconnect(m_mediaStream, &QIODevice::readyRead, this, &AVFMediaPlayer::streamReady);
1400 disconnect(m_mediaStream, &QIODevice::destroyed, this, &AVFMediaPlayer::streamDestroyed);
1401 }
1402
1403 m_mediaStream = stream;
1404
1405 if (m_mediaStream) {
1406 connect(m_mediaStream, &QIODevice::readyRead, this, &AVFMediaPlayer::streamReady);
1407 connect(m_mediaStream, &QIODevice::destroyed, this, &AVFMediaPlayer::streamDestroyed);
1408 }
1409}
1410
1411void AVFMediaPlayer::applyPitchCompensation(bool enabled)
1412{
1413 AVPlayerItem *playerItem = [m_observer playerItem];
1414 if (playerItem) {
1415 if (enabled)
1416 playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmSpectral;
1417 else
1418 playerItem.audioTimePitchAlgorithm = AVAudioTimePitchAlgorithmVarispeed;
1419 }
1420}
1421
1422void AVFMediaPlayer::resetBufferProgress()
1423{
1424 if (m_bufferProgress != 0) {
1425 m_bufferProgress = 0;
1426 bufferProgressChanged(0);
1427 }
1428}
1429
1431{
1432 // AVPlayerItem reports a zero presentationSize until the size is known
1433 // (and for audio-only media); don't propagate it as a real size.
1434 if (size.isEmpty() || size == m_nativeSize)
1435 return;
1436
1437 m_nativeSize = size;
1438 if (!m_videoSink)
1439 return;
1440 m_videoSink->setNativeSize(size);
1441}
1442
1443void AVFMediaPlayer::orientationChanged(QtVideo::Rotation rotation, bool mirrored)
1444{
1445 if (!m_videoOutput)
1446 return;
1447
1448 m_videoOutput->setVideoRotation(rotation);
1449 m_videoOutput->setVideoMirrored(mirrored);
1450}
1451
1452void AVFMediaPlayer::videoOrientationForAssetTrack(AVAssetTrack *videoTrack,
1453 QtVideo::Rotation &angle,
1454 bool &mirrored)
1455{
1456 angle = QtVideo::Rotation::None;
1457 mirrored = false;
1458 if (videoTrack) {
1459 CGAffineTransform transform = videoTrack.preferredTransform;
1460 if (CGAffineTransformIsIdentity(transform))
1461 return;
1462
1463 // determinant < 0 means the transform includes a reflection (mirror)
1464 qreal det = transform.a * transform.d - transform.b * transform.c;
1465 mirrored = (det < 0.0);
1466
1467 // Factor out mirror before computing rotation angle.
1468 // Negating the first column of a mirrored matrix yields a pure rotation.
1469 qreal ra = mirrored ? -transform.a : transform.a;
1470 qreal rb = mirrored ? -transform.b : transform.b;
1471
1472 qreal degrees = qRadiansToDegrees(qAtan2(rb, ra));
1473 if (degrees < 0)
1474 degrees += 360.0;
1475
1476 if (QtPrivate::fuzzyCompare(degrees, qreal(90))
1477 || QtPrivate::fuzzyCompare(degrees, qreal(-270))) {
1478 angle = QtVideo::Rotation::Clockwise90;
1479 } else if (QtPrivate::fuzzyCompare(degrees, qreal(-90))
1480 || QtPrivate::fuzzyCompare(degrees, qreal(270))) {
1481 angle = QtVideo::Rotation::Clockwise270;
1482 } else if (QtPrivate::fuzzyCompare(degrees, qreal(180))
1483 || QtPrivate::fuzzyCompare(degrees, qreal(-180))) {
1484 angle = QtVideo::Rotation::Clockwise180;
1485 }
1486 }
1487}
1488
1490{
1491 if (m_pitchCompensationEnabled == enabled)
1492 return;
1493
1494 applyPitchCompensation(enabled);
1495
1496 m_pitchCompensationEnabled = enabled;
1497 pitchCompensationChanged(enabled);
1498}
1499
1501{
1502 return m_pitchCompensationEnabled;
1503}
1504
1507{
1508 return QPlatformMediaPlayer::PitchCompensationAvailability::Available;
1509}
1510
1511#include "moc_avfmediaplayer_p.cpp"
static void * AVFMediaPlayerObserverCurrentItemObservationContext
static NSString *const AVF_BUFFER_LIKELY_KEEP_UP_KEY
static void * AVFMediaPlayerObserverPresentationSizeContext
static void * AVFMediaPlayerObserverTracksContext
static NSString *const AVF_STATUS_KEY
static NSString *const AVF_CURRENT_ITEM_DURATION_KEY
static void * AVFMediaPlayerObserverRateObservationContext
static void setURL(AVFMediaPlayerObserver *observer, const QUrl &url, const QString &mimeType=QString())
static NSString *const AVF_CURRENT_ITEM_KEY
static NSString *const AVF_PLAYABLE_KEY
static void * AVFMediaPlayerObserverBufferLikelyToKeepUpContext
static NSString *const AVF_RATE_KEY
static void * AVFMediaPlayerObserverCurrentItemDurationObservationContext
static void * AVFMediaPlayerObserverStatusObservationContext
QMediaMetaData trackMetaData(TrackType type, int trackNumber) override
qint64 duration() const override
void setVolume(float volume)
void processLoadStateChange()
int trackCount(TrackType) override
void setActiveTrack(TrackType type, int index) override
void setPosition(qint64 pos) override
void setVideoSink(QVideoSink *sink) override
QMediaTimeRange availablePlaybackRanges() const override
void nativeSizeChanged(QSize size)
QMediaMetaData metaData() const override
void setVideoOutput(AVFVideoRendererControl *output)
void stop() override
float bufferProgress() const override
void processMediaLoadError(QMediaPlayer::Error errorCode)
void setMedia(const QUrl &content, QIODevice *stream) override
qint64 position() const override
int activeTrack(TrackType type) override
void setPitchCompensation(bool enabled) override
void setMuted(bool muted)
void pause() override
void play() override
void processDurationChange(qint64 duration)
QUrl media() const override
bool pitchCompensation() const override
void subtitleOptionsLoaded(int generation, AVMediaSelectionGroup *group)
void processLoadStateFailure()
void updateAudioOutputDevice()
qreal playbackRate() const override
void processLoadStateChange(QMediaPlayer::PlaybackState newState)
QIODevice * mediaStream() const override
PitchCompensationAvailability pitchCompensationAvailability() const override
~AVFMediaPlayer() override
AVAsset * currentAssetHandle()
void setAudioOutput(QPlatformAudioOutput *output) override
void processBufferStateChange(int bufferProgress)
void setVideoSink(AVFVideoSink *sink)