diff --git a/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/Contents.json b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..6fab6174b995d35f5b67554223d3e8a825632dcf --- /dev/null +++ b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pip.enter.24x24.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pip.enter.24x24@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pip.enter.24x24@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24.png b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..9be881d5ef21709c9a7618790055792bb7e7338f Binary files /dev/null and b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24.png differ diff --git a/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24@2x.png b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b0f011328ea45f444bbe1e1321a9140efe88368b Binary files /dev/null and b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24@2x.png differ diff --git a/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24@3x.png b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c93663fb47704e1b849aa25906fd642200c01d93 Binary files /dev/null and b/Resources/iOS/Images.xcassets/NewPlayer/pip.enter.imageset/pip.enter.24x24@3x.png differ diff --git a/Sources/Playback/Control/PictureInPictureMediaController.swift b/Sources/Playback/Control/PictureInPictureMediaController.swift new file mode 100644 index 0000000000000000000000000000000000000000..b13e7ddffc2e5d95f39077533d7aac995997bb47 --- /dev/null +++ b/Sources/Playback/Control/PictureInPictureMediaController.swift @@ -0,0 +1,53 @@ +/***************************************************************************** + * PictureInPictureMediaController.swift + * VLC for iOS + ***************************************************************************** + * Copyright (c) 2025 VLC authors and VideoLAN + * $Id$ + * + * Authors: Maxime Chapelet <umxprime # videolabs.io> + * + * Refer to the COPYING file of the official project for license. + *****************************************************************************/ + +import Foundation +import VLCKit + +@objc(VLCPictureInPictureMediaController) +final class PictureInPictureMediaController: NSObject { + private let mediaPlayer: VLCMediaPlayer + @objc(initWithMediaPlayer:) + init(_ mediaPlayer: VLCMediaPlayer) { + self.mediaPlayer = mediaPlayer + } +} + +extension PictureInPictureMediaController: VLCPictureInPictureMediaControlling { + func play() { + mediaPlayer.play() + } + + func pause() { + mediaPlayer.pause() + } + + func seek(by offset: Int64, completion: (() -> Void)!) { + mediaPlayer.jump(withOffset: Int32(offset), completion: completion) + } + + func mediaLength() -> Int64 { + return mediaPlayer.media?.length.value?.int64Value ?? 0 + } + + func mediaTime() -> Int64 { + return mediaPlayer.time.value?.int64Value ?? 0 + } + + func isMediaSeekable() -> Bool { + return mediaPlayer.isSeekable + } + + func isMediaPlaying() -> Bool { + return mediaPlayer.isPlaying + } +} diff --git a/Sources/Playback/Control/VLCPlaybackService.h b/Sources/Playback/Control/VLCPlaybackService.h index 3e7cb019ab3b5e060e710bb90cef7050d03c5e75..f4fc636e239b371f07baacacc3e9add11e39de8c 100644 --- a/Sources/Playback/Control/VLCPlaybackService.h +++ b/Sources/Playback/Control/VLCPlaybackService.h @@ -168,6 +168,7 @@ NS_SWIFT_NAME(PlaybackService) - (void)addSubtitlesToCurrentPlaybackFromURL:(NSURL *)subtitleURL; - (void)setAmplification:(CGFloat)amplification forBand:(unsigned int)index; +- (void)togglePictureInPicture; #if TARGET_OS_IOS - (void)savePlaybackState; diff --git a/Sources/Playback/Control/VLCPlaybackService.m b/Sources/Playback/Control/VLCPlaybackService.m index 4941fd6303209936ea673ebef6a517b6e9e89459..362b6de28562fbb6de4037e99c51ca517bd7f99e 100644 --- a/Sources/Playback/Control/VLCPlaybackService.m +++ b/Sources/Playback/Control/VLCPlaybackService.m @@ -2,7 +2,7 @@ * VLCPlaybackService.m * VLC for iOS ***************************************************************************** - * Copyright (c) 2013-2023 VLC authors and VideoLAN + * Copyright (c) 2013-2025 VLC authors and VideoLAN * $Id$ * * Authors: Felix Paul Kühne <fkuehne # videolan.org> @@ -44,9 +44,9 @@ NSString *const VLCPlaybackServiceShuffleModeUpdated = @"VLCPlaybackServiceShuff NSString *const VLCPlaybackServicePlaybackDidMoveOnToNextItem = @"VLCPlaybackServicePlaybackDidMoveOnToNextItem"; #if TARGET_OS_IOS -@interface VLCPlaybackService () <VLCMediaPlayerDelegate, VLCMediaDelegate, VLCMediaListPlayerDelegate, EqualizerViewDelegate> +@interface VLCPlaybackService () <VLCMediaPlayerDelegate, VLCMediaDelegate, VLCMediaListPlayerDelegate, EqualizerViewDelegate, VLCDrawable, VLCPictureInPictureDrawable> #else -@interface VLCPlaybackService () <VLCMediaPlayerDelegate, VLCMediaDelegate, VLCMediaListPlayerDelegate> +@interface VLCPlaybackService () <VLCMediaPlayerDelegate, VLCMediaDelegate, VLCMediaListPlayerDelegate, VLCDrawable, VLCPictureInPictureDrawable> #endif { VLCMediaPlayer *_backgroundDummyPlayer; @@ -86,6 +86,9 @@ NSString *const VLCPlaybackServicePlaybackDidMoveOnToNextItem = @"VLCPlaybackSer BOOL _openInMiniPlayer; } +@property (weak, atomic) id<VLCPictureInPictureWindowControlling> pipController; +@property (atomic) id<VLCPictureInPictureMediaControlling> mediaController; + @end @implementation VLCPlaybackService @@ -251,9 +254,9 @@ NSString *const VLCPlaybackServicePlaybackDidMoveOnToNextItem = @"VLCPlaybackSer } if (libVLCOptions.count > 0) { _listPlayer = [[VLCMediaListPlayer alloc] initWithOptions:libVLCOptions - andDrawable:_actualVideoOutputView]; + andDrawable:self]; } else { - _listPlayer = [[VLCMediaListPlayer alloc] initWithDrawable:_actualVideoOutputView]; + _listPlayer = [[VLCMediaListPlayer alloc] initWithDrawable:self]; } _listPlayer.delegate = self; @@ -298,6 +301,9 @@ NSString *const VLCPlaybackServicePlaybackDidMoveOnToNextItem = @"VLCPlaybackSer newFilter.enabled = _adjustFilter.mediaPlayerAdjustFilter.isEnabled; _adjustFilter = [[VLCPlaybackServiceAdjustFilter alloc] initWithMediaPlayerAdjustFilter:newFilter]; _mediaPlayer = _listPlayer.mediaPlayer; +#if TARGET_OS_IOS + _mediaController = [[VLCPictureInPictureMediaController alloc] initWithMediaPlayer:_mediaPlayer]; +#endif [_mediaPlayer setDelegate:self]; CGFloat defaultPlaybackSpeed = [[defaults objectForKey:kVLCSettingPlaybackSpeedDefaultValue] floatValue]; @@ -827,6 +833,10 @@ NSString *const VLCPlaybackServicePlaybackDidMoveOnToNextItem = @"VLCPlaybackSer - (void)mediaPlayerStateChanged:(VLCMediaPlayerState)currentState { + id<VLCPictureInPictureWindowControlling> pipController = _pipController; + dispatch_async(dispatch_get_main_queue(), ^{ + [pipController invalidatePlaybackState]; + }); switch (currentState) { case VLCMediaPlayerStateBuffering: { /* attach delegate */ @@ -1830,4 +1840,27 @@ NSString *const VLCPlaybackServicePlaybackDidMoveOnToNextItem = @"VLCPlaybackSer object:self]; } +#pragma mark - VLCDrawable + +- (void)addSubview:(UIView *)view { + [_actualVideoOutputView addSubview:view]; +} + +- (CGRect)bounds { + return [_actualVideoOutputView bounds]; +} + +#pragma mark - VLCPictureInPictureDrawable + +- (void (^)(id<VLCPictureInPictureWindowControlling>))pictureInPictureReady { + __weak typeof(self) drawable = self; + return ^(id<VLCPictureInPictureWindowControlling> pipController){ + drawable.pipController = pipController; + }; +} + +- (void)togglePictureInPicture { + [self.pipController startPictureInPicture]; +} + @end diff --git a/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaNavigationBar.swift b/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaNavigationBar.swift index b3ee0ded4bc4acebbb375ac305a22b8f8d1edea9..8962a0fb97f3087f8f062ea9fac3347875c7dbdb 100644 --- a/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaNavigationBar.swift +++ b/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaNavigationBar.swift @@ -16,6 +16,7 @@ import MediaPlayer @objc (VLCMediaNavigationBarDelegate) protocol MediaNavigationBarDelegate { func mediaNavigationBarDidTapClose(_ mediaNavigationBar: MediaNavigationBar) + @objc optional func mediaNavigationBarDidTapPictureInPicture(_ mediaNavigationBar: MediaNavigationBar) @objc optional func mediaNavigationBarDidToggleQueueView(_ mediaNavigationBar: MediaNavigationBar) @objc optional func mediaNavigationBarDidToggleChromeCast(_ mediaNavigationBar: MediaNavigationBar) func mediaNavigationBarDidCloseLongPress(_ mediaNavigationBar: MediaNavigationBar) @@ -104,6 +105,16 @@ private enum RendererActionSheetContent: Int, CaseIterable { return chromeButton }() + lazy var pictureInPictureButton: UIButton = { + var button = UIButton(type: .system) + button.addTarget(self, action: #selector(togglePictureInPicture), + for: .touchDown) + button.setImage(UIImage(named: "pip.enter"), for: .normal) + button.tintColor = .white + button.setContentHuggingPriority(.defaultHigh, for: .horizontal) + return button + }() + private var closureQueue: (() -> Void)? = nil private lazy var deviceActionSheet: ActionSheet = { @@ -187,6 +198,7 @@ private enum RendererActionSheetContent: Int, CaseIterable { addArrangedSubview(rotateButton) addArrangedSubview(queueButton) addArrangedSubview(deviceButton) + addArrangedSubview(pictureInPictureButton) } // MARK: Gesture recognizer @@ -210,6 +222,10 @@ private enum RendererActionSheetContent: Int, CaseIterable { animated: true) } + func togglePictureInPicture() { + delegate?.mediaNavigationBarDidTapPictureInPicture?(self) + } + func handleCloseTap() { assert(delegate != nil, "Delegate not set for MediaNavigationBar") delegate?.mediaNavigationBarDidTapClose(self) diff --git a/Sources/Playback/Player/VideoPlayer-iOS/VideoPlayerViewController.swift b/Sources/Playback/Player/VideoPlayer-iOS/VideoPlayerViewController.swift index 1388c3a091bffc715ebd1fe5246f6e068883c905..25e428b44f910559cfdc7507fed46f5a428661e5 100644 --- a/Sources/Playback/Player/VideoPlayer-iOS/VideoPlayerViewController.swift +++ b/Sources/Playback/Player/VideoPlayer-iOS/VideoPlayerViewController.swift @@ -1453,6 +1453,10 @@ extension VideoPlayerViewController { func mediaNavigationBarDisplayCloseAlert(_ mediaNavigationBar: MediaNavigationBar) { statusLabel.showStatusMessage(NSLocalizedString("MINIMIZE_HINT", comment: "")) } + + func mediaNavigationBarDidTapPictureInPicture(_ mediaNavigationBar: MediaNavigationBar) { + playbackService.togglePictureInPicture() + } } // MARK: - MediaScrubProgressBarDelegate diff --git a/VLC.xcodeproj/project.pbxproj b/VLC.xcodeproj/project.pbxproj index 0eb5a9afbd23628566a232ede42499d6b6c8ecb8..0d48f6bccf907fa4ffbc7b7aecc9c564c3364509 100644 --- a/VLC.xcodeproj/project.pbxproj +++ b/VLC.xcodeproj/project.pbxproj @@ -104,6 +104,7 @@ 597B403F2625E85000C0D81E /* SliderInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 597B403E2625E85000C0D81E /* SliderInfoView.swift */; }; 6C5B0C9E27A43098005AE25B /* PlaybackServiceAdjustFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5B0C9C27A43098005AE25B /* PlaybackServiceAdjustFilter.swift */; }; 6C5B0C9F27A46258005AE25B /* PlaybackServiceAdjustFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C5B0C9C27A43098005AE25B /* PlaybackServiceAdjustFilter.swift */; }; + 6CC3F6B32D230AEF00C15E33 /* PictureInPictureMediaController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CC3F6B22D230AEF00C15E33 /* PictureInPictureMediaController.swift */; }; 6D0B038825E7CBF90013DEF4 /* PopupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D0B038725E7CBF90013DEF4 /* PopupView.swift */; }; 6D3C676C23CDF1FC0039ACFD /* public in Resources */ = {isa = PBXBuildFile; fileRef = 6D3C676B23CDF1FC0039ACFD /* public */; }; 6D4756B123607D4A005F670E /* EditActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D4756B023607D49005F670E /* EditActions.swift */; }; @@ -688,6 +689,7 @@ 57087A12E77ACEB9D1D30E33 /* Pods-VLC-tvOS.distribution.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-VLC-tvOS.distribution.xcconfig"; path = "Target Support Files/Pods-VLC-tvOS/Pods-VLC-tvOS.distribution.xcconfig"; sourceTree = "<group>"; }; 597B403E2625E85000C0D81E /* SliderInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderInfoView.swift; sourceTree = "<group>"; }; 6C5B0C9C27A43098005AE25B /* PlaybackServiceAdjustFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PlaybackServiceAdjustFilter.swift; path = Sources/Playback/Control/PlaybackServiceAdjustFilter.swift; sourceTree = SOURCE_ROOT; }; + 6CC3F6B22D230AEF00C15E33 /* PictureInPictureMediaController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureMediaController.swift; sourceTree = "<group>"; }; 6D0B038725E7CBF90013DEF4 /* PopupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupView.swift; sourceTree = "<group>"; }; 6D3C676B23CDF1FC0039ACFD /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; }; 6D4756B023607D49005F670E /* EditActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditActions.swift; sourceTree = "<group>"; }; @@ -1738,6 +1740,7 @@ 418DFE9E211C93C6005D3652 /* CustomDialogRendererHandler.swift */, 6C5B0C9C27A43098005AE25B /* PlaybackServiceAdjustFilter.swift */, 8D43712C2056AF1600F36458 /* VLCRendererDiscovererManager.swift */, + 6CC3F6B22D230AEF00C15E33 /* PictureInPictureMediaController.swift */, ); path = Control; sourceTree = "<group>"; @@ -3859,6 +3862,7 @@ 91C1BB8025EFD7A40096F97E /* ColorThemeExtension.swift in Sources */, 7D3784C2183A9938009EE944 /* VLCSlider.m in Sources */, 7D3784C3183A9938009EE944 /* VLCStatusLabel.m in Sources */, + 6CC3F6B32D230AEF00C15E33 /* PictureInPictureMediaController.swift in Sources */, 4144156C20ECE6330078EC37 /* FileServerView.swift in Sources */, 40C95A07256E929D002DD208 /* PlaybackSpeedView.swift in Sources */, 416DACB720B6DB9A001BC75D /* PlayingExternallyView.swift in Sources */,