Skip to content

Commit

Permalink
Merge pull request #24 from apivideo/feat/display-info-center
Browse files Browse the repository at this point in the history
feat() add mediaplayer controller to control video from lockscreen
  • Loading branch information
RomainPetit1 authored May 10, 2023
2 parents b14e0a6 + 9c9c98c commit 7b0c9c2
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
C734306628F453F900A82721 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
C734306928F453F900A82721 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
C734307128F4540D00A82721 /* api.video-swift-player */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "api.video-swift-player"; path = ../..; sourceTree = "<group>"; };
C77D97A629F2CB7C00987B1F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -55,6 +56,7 @@
C734306128F453F900A82721 /* PlayerSwiftUI */ = {
isa = PBXGroup;
children = (
C77D97A629F2CB7C00987B1F /* Info.plist */,
C734306228F453F900A82721 /* PlayerSwiftUIApp.swift */,
C734306428F453F900A82721 /* ContentView.swift */,
C734306628F453F900A82721 /* Assets.xcassets */,
Expand Down Expand Up @@ -285,6 +287,7 @@
DEVELOPMENT_TEAM = VY3VXRC7P4;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PlayerSwiftUI/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down Expand Up @@ -315,6 +318,7 @@
DEVELOPMENT_TEAM = VY3VXRC7P4;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = PlayerSwiftUI/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ class PlayerViewController: UIViewController {
view.addSubview(self.scrollView)
self.scrollView.addSubview(self.playerView)
self.playerView.addDelegate(self)
self.playerView.enableRemoteControl = true
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:)))
doubleTap.numberOfTapsRequired = 2
self.playerView.addGestureRecognizer(doubleTap)
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ a video Id to use this component and play a video from api.video. To get yours,
Alternatively, you can find your video Id in the video details of
your [dashboard](https://dashboard.api.video).

## Code sample

## Code sample

Expand Down Expand Up @@ -180,6 +179,19 @@ override func viewDidAppear(_ animated: Bool) {
}
```

### Remote control

If you want to enable the remote control do the following:
```swift
override func viewDidLoad() {
...
self.playerView.enableRemoteControl = true
}
```
When you have to remove it set `enableRemoteControl` to false

By default the remote control is hidden.

# Sample application

A demo application demonstrates how to use player.
Expand All @@ -192,7 +204,7 @@ On the first run, you will have to set your video Id:

# Documentation

* [API documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/)
* [Player documentation](https://apivideo.github.io/api.video-swift-player/documentation/apivideoplayer/)
* [api.video documentation](https://docs.api.video)

# Dependencies
Expand Down
88 changes: 85 additions & 3 deletions Sources/ApiVideoPlayer/ApiVideoPlayerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ApiVideoPlayerAnalytics
import AVFoundation
import AVKit
import Foundation
import MediaPlayer

/// The ApiVideoPlayerController class is a wrapper around ``AVPlayer``.
/// It is used internally of the ``ApiVideoPlayerView``.
Expand All @@ -16,6 +17,7 @@ public class ApiVideoPlayerController: NSObject {
private let multicastDelegate = ApiVideoPlayerControllerMulticastDelegate()
private var playerItemFactory: ApiVideoPlayerItemFactory?
private var storedSpeedRate: Float = 1.0
private var infoNowPlaying: ApiVideoPlayerInformationNowPlaying

#if !os(macOS)
/// Initializes a player controller.
Expand Down Expand Up @@ -54,12 +56,12 @@ public class ApiVideoPlayerController: NSObject {
) {
multicastDelegate.addDelegates(delegates)
self.taskExecutor = taskExecutor
self.infoNowPlaying = ApiVideoPlayerInformationNowPlaying(taskExecutor: taskExecutor)

super.init()
defer {
self.videoOptions = videoOptions
}

self.autoplay = autoplay
self.avPlayer.addObserver(
self,
Expand All @@ -73,6 +75,16 @@ public class ApiVideoPlayerController: NSObject {
options: NSKeyValueObservingOptions.new,
context: nil
)
if #available(iOS 15.0, macOS 12.0, *) {
NotificationCenter.default.addObserver(
self,
selector: #selector(handlePlaybackRateChange(_:)),
name: AVPlayer.rateDidChangeNotification,
object: self.avPlayer
)
} else {
// Fallback on earlier versions
}
}

private func retrySetUpPlayerUrlWithMp4() {
Expand Down Expand Up @@ -218,14 +230,15 @@ public class ApiVideoPlayerController: NSObject {
self.avPlayer.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero) { completed in
self.analytics?
.seek(
from: Float(CMTimeGetSeconds(from)),
to: Float(CMTimeGetSeconds(self.currentTime))
from: Float(from.seconds),
to: Float(time.seconds)
) { result in
switch result {
case .success: break
case let .failure(error): print("analytics error on seek event: \(error)")
}
}
self.infoNowPlaying.updateCurrentTime(currentTime: time)
completion(completed)
}
}
Expand Down Expand Up @@ -276,6 +289,7 @@ public class ApiVideoPlayerController: NSObject {
/// Gets and sets the video options.
public var videoOptions: VideoOptions? {
didSet {
self.isFirstPlay = true
guard let videoOptions = videoOptions else {
resetPlayer(with: nil)
return
Expand Down Expand Up @@ -415,6 +429,24 @@ public class ApiVideoPlayerController: NSObject {
if isPlaying {
avPlayer.rate = newRate
}
if #available(iOS 15, *) {
// do nothing Notification will handle updatePlaybackRate
} else {
// iOS version is less than iOS 15
infoNowPlaying.updatePlaybackRate(rate: newRate)
}
}
}

public var enableRemoteControl = false {
didSet {
if enableRemoteControl {
self.setupRemoteControls()
} else {
#if !os(macOS)
UIApplication.shared.endReceivingRemoteControlEvents()
#endif
}
}
}

Expand Down Expand Up @@ -446,6 +478,8 @@ public class ApiVideoPlayerController: NSObject {
public func goToFullScreen(viewController: UIViewController) {
let playerViewController = AVPlayerViewController()
playerViewController.player = self.avPlayer
// set updatesNowPlayingInfoCenter to false to avoid issue with artwork (blink when play/pause video)
playerViewController.updatesNowPlayingInfoCenter = false
viewController.present(playerViewController, animated: true) {
self.play()
}
Expand All @@ -467,6 +501,30 @@ public class ApiVideoPlayerController: NSObject {
self.multicastDelegate.didEnd()
}

private func setupRemoteControls() {
let rcc = MPRemoteCommandCenter.shared()
rcc.skipForwardCommand.preferredIntervals = [15.0]
rcc.skipBackwardCommand.preferredIntervals = [15.0]
rcc.skipForwardCommand.addTarget { event in
guard let event = event as? MPSkipIntervalCommandEvent else { return .commandFailed }
self.seek(offset: CMTime(seconds: event.interval, preferredTimescale: 1_000))
return .success
}
rcc.skipBackwardCommand.addTarget { event in
guard let event = event as? MPSkipIntervalCommandEvent else { return .commandFailed }
self.seek(offset: CMTime(seconds: -event.interval, preferredTimescale: 1_000))
return .success
}
rcc.playCommand.addTarget { _ in
self.play()
return .success
}
rcc.pauseCommand.addTarget { _ in
self.pause()
return .success
}
}

private func doFallbackOnFailed() {
if self.avPlayer.currentItem?.status == .failed {
guard let url = (avPlayer.currentItem?.asset as? AVURLAsset)?.url else {
Expand Down Expand Up @@ -513,6 +571,7 @@ public class ApiVideoPlayerController: NSObject {
case let .failure(error): print("analytics error on pause event: \(error)")
}
}
self.infoNowPlaying.pause(currentTime: self.currentTime)
self.multicastDelegate.didPause()
}

Expand All @@ -523,6 +582,16 @@ public class ApiVideoPlayerController: NSObject {
}
if self.isFirstPlay {
self.isFirstPlay = false
#if !os(macOS)
self.infoNowPlaying.nowPlayingData = NowPlayingData(
duration: self.duration,
currentTime: self.currentTime,
isLive: self.isLive,
thumbnailUrl: self.videoOptions?.thumbnailUrl,
playbackRate: self.avPlayer.rate
)

#endif
self.analytics?.play { result in
switch result {
case .success: break
Expand All @@ -536,10 +605,23 @@ public class ApiVideoPlayerController: NSObject {
case let .failure(error): print("analytics error on resume event: \(error)")
}
}
self.infoNowPlaying.play(currentTime: self.currentTime)
}
#if !os(macOS)
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback)
try? AVAudioSession.sharedInstance().setActive(true)
#endif
self.multicastDelegate.didPlay()
}

@objc
func handlePlaybackRateChange(_ notification: Notification) {
guard let player = notification.object as? AVPlayer else {
return
}
infoNowPlaying.updatePlaybackRate(rate: player.rate)
}

private func doTimeControlStatus() {
let status = self.avPlayer.timeControlStatus
switch status {
Expand Down
88 changes: 88 additions & 0 deletions Sources/ApiVideoPlayer/ApiVideoPlayerInformationNowPlaying.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Foundation
import MediaPlayer

protocol InformationNowPlaying {
var nowPlayingData: NowPlayingData? { get set }
func pause(currentTime: CMTime)
func play(currentTime: CMTime)
func updateCurrentTime(currentTime: CMTime)
func updatePlaybackRate(rate: Float)
}

class ApiVideoPlayerInformationNowPlaying: InformationNowPlaying {
private var infos = [String: Any]()
private let taskExecutor: TasksExecutorProtocol.Type

init(taskExecutor: TasksExecutorProtocol.Type) {
self.taskExecutor = taskExecutor
}

var nowPlayingData: NowPlayingData? {
didSet {
guard let nowPlayingData = nowPlayingData else {
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
return
}
if let title = nowPlayingData.title {
infos[MPMediaItemPropertyTitle] = title
}
infos[MPMediaItemPropertyPlaybackDuration] = nowPlayingData.duration.seconds
infos[MPNowPlayingInfoPropertyElapsedPlaybackTime] = nowPlayingData.currentTime.seconds
infos[MPNowPlayingInfoPropertyIsLiveStream] = nowPlayingData.isLive

infos[MPNowPlayingInfoPropertyPlaybackRate] = nowPlayingData.playbackRate
#if !os(macOS)
if let thumb = nowPlayingData.thumbnailUrl {
if let url = URL(string: thumb) {
updateRemoteArtwork(url: url)
}
}
#endif
MPNowPlayingInfoCenter.default().nowPlayingInfo = infos
}
}

func updateCurrentTime(currentTime: CMTime) {
self.overrideInformations(
for: MPNowPlayingInfoPropertyElapsedPlaybackTime,
value: currentTime.seconds
)
}

func updatePlaybackRate(rate: Float) {
self.overrideInformations(
for: MPNowPlayingInfoPropertyPlaybackRate,
value: rate
)
}

func pause(currentTime: CMTime) {
MPNowPlayingInfoCenter.default().playbackState = .paused
self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds)
}

func play(currentTime: CMTime) {
MPNowPlayingInfoCenter.default().playbackState = .playing
self.overrideInformations(for: MPNowPlayingInfoPropertyElapsedPlaybackTime, value: currentTime.seconds)
}

private func overrideInformations(for key: String, value: Any) {
infos[key] = value
MPNowPlayingInfoCenter.default().nowPlayingInfo = infos
}

#if !os(macOS)
private func getArtwork(image: UIImage) -> MPMediaItemArtwork {
return MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}

private func updateRemoteArtwork(url: URL) {
RequestsBuilder.getThumbnail(taskExecutor: self.taskExecutor, url: url, completion: { image in
let artwork = self.getArtwork(image: image)
self.overrideInformations(for: MPMediaItemPropertyArtwork, value: artwork)
}, didError: { error in
print("Error on artwork : \(error.localizedDescription)")
})
}
#endif
}
11 changes: 11 additions & 0 deletions Sources/ApiVideoPlayer/Models/NowPlayingData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import CoreMedia
import Foundation

struct NowPlayingData {
let duration: CMTime
let currentTime: CMTime
let isLive: Bool
let thumbnailUrl: String?
let title: String? = nil
let playbackRate: Float
}
Loading

0 comments on commit 7b0c9c2

Please sign in to comment.