Skip to content

Commit

Permalink
- Update Clips-specific example project to conform to new video playe…
Browse files Browse the repository at this point in the history
…r apis (#199)

* - Update Clips-specific example project to conform to new video player APIs
  • Loading branch information
ALexanderLonsky authored Mar 15, 2022
1 parent 46a4967 commit 3319ef7
Show file tree
Hide file tree
Showing 9 changed files with 759 additions and 371 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
02152A4426BC57FF0025E180 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 02152A4226BC57FF0025E180 /* LaunchScreen.storyboard */; };
02152A4D26BC58220025E180 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 02152A4C26BC58220025E180 /* GiphyUISDK */; };
02152A5426BC58E50025E180 /* VideoLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02152A5226BC58E50025E180 /* VideoLoadingView.swift */; };
02152A5526BC58E50025E180 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02152A5326BC58E50025E180 /* VideoView.swift */; };
0252E9A3273C6A5800B6910A /* ToggleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0252E9A2273C6A5800B6910A /* ToggleButton.swift */; };
FACF9A5627DF65E10050FE90 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF9A5527DF65E10050FE90 /* VideoPlayerView.swift */; };
FACF9A5827DF67500050FE90 /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF9A5727DF67500050FE90 /* VideoPlayer.swift */; };
FACF9A5C27DF8F500050FE90 /* VideoPlayer+Observers.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF9A5B27DF8F500050FE90 /* VideoPlayer+Observers.swift */; };
FACF9A5F27DF90000050FE90 /* VideoPlayerView+Listener.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF9A5D27DF90000050FE90 /* VideoPlayerView+Listener.swift */; };
FACF9A6027DF90000050FE90 /* VideoPlayerView+Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FACF9A5E27DF90000050FE90 /* VideoPlayerView+Captions.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand All @@ -29,9 +33,13 @@
02152A4326BC57FF0025E180 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
02152A4526BC57FF0025E180 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
02152A5226BC58E50025E180 /* VideoLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoLoadingView.swift; sourceTree = "<group>"; };
02152A5326BC58E50025E180 /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
0252E9A2273C6A5800B6910A /* ToggleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToggleButton.swift; sourceTree = "<group>"; };
02B0D32426BC742B00E0D026 /* video-player-sample.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "video-player-sample.md"; sourceTree = "<group>"; };
FACF9A5527DF65E10050FE90 /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayerView.swift; path = "clips-example/VideoPlayerView.swift"; sourceTree = SOURCE_ROOT; };
FACF9A5727DF67500050FE90 /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = VideoPlayer.swift; path = "clips-example/VideoPlayer.swift"; sourceTree = SOURCE_ROOT; };
FACF9A5B27DF8F500050FE90 /* VideoPlayer+Observers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "VideoPlayer+Observers.swift"; path = "clips-example/VideoPlayer+Observers.swift"; sourceTree = SOURCE_ROOT; };
FACF9A5D27DF90000050FE90 /* VideoPlayerView+Listener.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "VideoPlayerView+Listener.swift"; path = "clips-example/VideoPlayerView+Listener.swift"; sourceTree = SOURCE_ROOT; };
FACF9A5E27DF90000050FE90 /* VideoPlayerView+Captions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "VideoPlayerView+Captions.swift"; path = "clips-example/VideoPlayerView+Captions.swift"; sourceTree = SOURCE_ROOT; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -68,7 +76,11 @@
children = (
0252E9A2273C6A5800B6910A /* ToggleButton.swift */,
02152A5226BC58E50025E180 /* VideoLoadingView.swift */,
02152A5326BC58E50025E180 /* VideoView.swift */,
FACF9A5527DF65E10050FE90 /* VideoPlayerView.swift */,
FACF9A5E27DF90000050FE90 /* VideoPlayerView+Captions.swift */,
FACF9A5D27DF90000050FE90 /* VideoPlayerView+Listener.swift */,
FACF9A5727DF67500050FE90 /* VideoPlayer.swift */,
FACF9A5B27DF8F500050FE90 /* VideoPlayer+Observers.swift */,
02152A3726BC57FE0025E180 /* AppDelegate.swift */,
02152A3926BC57FE0025E180 /* SceneDelegate.swift */,
02152A3B26BC57FE0025E180 /* ViewController.swift */,
Expand Down Expand Up @@ -156,12 +168,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FACF9A5827DF67500050FE90 /* VideoPlayer.swift in Sources */,
FACF9A6027DF90000050FE90 /* VideoPlayerView+Captions.swift in Sources */,
FACF9A5F27DF90000050FE90 /* VideoPlayerView+Listener.swift in Sources */,
02152A3C26BC57FE0025E180 /* ViewController.swift in Sources */,
0252E9A3273C6A5800B6910A /* ToggleButton.swift in Sources */,
02152A5526BC58E50025E180 /* VideoView.swift in Sources */,
02152A5426BC58E50025E180 /* VideoLoadingView.swift in Sources */,
02152A3826BC57FE0025E180 /* AppDelegate.swift in Sources */,
FACF9A5C27DF8F500050FE90 /* VideoPlayer+Observers.swift in Sources */,
02152A3A26BC57FE0025E180 /* SceneDelegate.swift in Sources */,
FACF9A5627DF65E10050FE90 /* VideoPlayerView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// VideoPlayer+Observers.swift
// Clips-Example
//
// Created by Alex on 11.3.2022.
// Copyright © 2022 GIPHY. All rights reserved.
//

import UIKit
import AVFoundation

extension VideoPlayer {

func removePlayerObservers() {
lock.lock()
defer { lock.unlock() }

guard let player = videoPlayer else { return }

player.removeObserver(self, forKeyPath: #keyPath(AVQueuePlayer.currentItem))
player.removeObserver(self, forKeyPath: #keyPath(AVQueuePlayer.currentItem.status))
player.removeObserver(self, forKeyPath: #keyPath(AVQueuePlayer.isMuted))
player.removeObserver(self, forKeyPath: #keyPath(AVQueuePlayer.timeControlStatus))
}

func addPlayerObservers() {
lock.lock()
defer { lock.unlock() }

guard let player = videoPlayer else { return }

player.addObserver(self, forKeyPath: #keyPath(AVQueuePlayer.currentItem), options: [.old, .new], context: &playerItemContext)
player.addObserver(self, forKeyPath: #keyPath(AVQueuePlayer.currentItem.status), options: [.old, .new], context: &playerItemStatusContext)
player.addObserver(self, forKeyPath: #keyPath(AVQueuePlayer.isMuted), options: [.old, .new], context: &playerMuteContext)
player.addObserver(self, forKeyPath: #keyPath(AVQueuePlayer.timeControlStatus), options: [.old, .new], context: &playerStatusContext)
}


public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

guard let player = videoPlayer else { return }

if context == &playerStatusContext {
if
let change = change,
let newValue = change[NSKeyValueChangeKey.newKey] as? Int,
let oldValue = change[NSKeyValueChangeKey.oldKey] as? Int {
let oldStatus = AVPlayer.TimeControlStatus(rawValue: oldValue)
let newStatus = AVPlayer.TimeControlStatus(rawValue: newValue)

if newStatus != oldStatus {
switch newStatus {
case .playing:
notifyListeners { $0.playerStateDidChange?(.playing) }
case .paused:
notifyListeners { $0.playerStateDidChange?(.paused) }
case .waitingToPlayAtSpecifiedRate:
notifyListeners { $0.playerStateDidChange?(.idle) }
default:
break
}
} else {
if newStatus == .playing {
notifyListeners { $0.playerStateDidChange?(.repeated) }
}
}
}
} else if context == &playerMuteContext {
notifyListeners { $0.muteDidChange?(isMuted: player.isMuted) }
} else if context == &playerItemContext {
} else if context == &playerItemStatusContext {
guard let currentItem = player.currentItem else { return }

switch currentItem.status {
case .failed:
notifyListeners { $0.playerDidFail?(currentItem.error?.localizedDescription) }
case .unknown:
notifyListeners { $0.playerStateDidChange?(.unknown) }
case .readyToPlay:
if firstStart {
firstStart = false
notifyListeners { $0.playerStateDidChange?(.readyToPlay) }
}
@unknown default:
break
}

} else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}


202 changes: 202 additions & 0 deletions Clips-Player-Sample-Project/clips-example/VideoPlayer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//
// VideoPlayer.swift
// Clips-Example
//
// Created by Alex on 11.3.2022.
// Copyright © 2022 GIPHY. All rights reserved.
//

import UIKit
import AVFoundation

@objc public class Media: NSObject {
public fileprivate(set) var videoUrl: String
public fileprivate(set) var imagePreviewUrl: String?
public fileprivate(set) var aspectRatio: CGFloat

public init(videoUrl: String, imagePreviewUrl: String?, aspectRatio: CGFloat) {
self.videoUrl = videoUrl
self.imagePreviewUrl = imagePreviewUrl
self.aspectRatio = aspectRatio
super.init()
}
}

@objc public enum VideoPlayerState: Int {
case unknown
case readyToPlay
case playing
case paused
case repeated
case idle
}

@objc public protocol VideoPlayerStateListener: AnyObject {
@objc optional func playerStateDidChange(_ state: VideoPlayerState)
@objc optional func playerDidFail(_ description: String?)
@objc optional func muteDidChange(isMuted: Bool)
@objc optional func mediaDidChange(media: Media?)
}

// maintain the caption state if you like
public class CaptionState: NSObject {
static let key = "kGPHClipsCaptionState"

public static var enabled: Bool {
guard let state = UserDefaults.standard.object(forKey: CaptionState.key) as? Bool else {
return false
}
return state
}

class func setEnabled(_ enabled: Bool) {
UserDefaults.standard.setValue(enabled, forKey: CaptionState.key)
UserDefaults.standard.synchronize()
}
}

@objcMembers
public class VideoPlayer: NSObject {
private var listeners = [VideoPlayerStateListener]()

private var videoPlayerLooper: AVPlayerLooper?
private(set) var videoPlayer: AVQueuePlayer?

private(set) var media: Media?

public weak var playerView: VideoPlayerView?

let lock = NSRecursiveLock()

var firstStart = false

var playerMuteContext = 0
var playerStatusContext = 0
var playerItemStatusContext = 0
var playerItemContext = 0

var repeatable: Bool = true

// MARK: -
// MARK: Init

public override init() {
super.init()
}

// MARK: -
// MARK: Actions

func notifyListeners(action: (VideoPlayerStateListener) -> Void) {
lock.lock()
defer { lock.unlock() }

listeners.forEach({
action($0)
})
}

deinit {
stop()
}
}

// MARK: -
// MARK: Public APIs
extension VideoPlayer {
public func add(listener: VideoPlayerStateListener) {
lock.lock()
defer { lock.unlock() }

if listeners.firstIndex(where: {$0 === listener}) == nil {
listeners.append(listener)
}

}

public func remove(listener: VideoPlayerStateListener) {
lock.lock()
defer { lock.unlock() }

guard let index = listeners.firstIndex(where: { $0 === listener }) else { return }
listeners.remove(at: index)
}

public func prepare(media: Media,
view: VideoPlayerView?) {
lock.lock()
defer { lock.unlock() }

self.playerView = view

view?.preloadFirstFrame(media: media, videoPlayer: self)
}

public func loadMedia(media: Media,
autoPlay: Bool = true,
muteOnPlay: Bool = false,
view: VideoPlayerView,
repeatable: Bool = true) {
lock.lock()
defer { lock.unlock() }

self.playerView = view

stop()

self.repeatable = repeatable
self.firstStart = true
self.media = media

notifyListeners(action: { $0.mediaDidChange?(media: media) })

guard let url = URL(string: media.videoUrl) else {
return
}

let asset = AVAsset(url: url)
let keys: [String] = ["playable"]
asset.loadValuesAsynchronously(forKeys: keys) { [weak self] in
DispatchQueue.main.async {
guard self?.media === media else { return }

let playerItem = AVPlayerItem(asset: asset)
let videoPlayer = AVQueuePlayer(items: [playerItem])
self?.videoPlayer = videoPlayer
if repeatable {
self?.videoPlayerLooper = AVPlayerLooper(player: videoPlayer, templateItem: playerItem)
}
self?.addPlayerObservers()

if (muteOnPlay) {
self?.mute(true)
}

if autoPlay {
videoPlayer.play()
}
}
}
view.prepare(media: media, videoPlayer: self)
}

public func pause() {
videoPlayer?.pause()
}

public func resume() {
videoPlayer?.play()
}

public func mute(_ isMuted: Bool) {
videoPlayer?.isMuted = isMuted
}

public func stop() {
removePlayerObservers()
videoPlayer?.pause()
videoPlayer = nil
videoPlayerLooper = nil
}

}
Loading

0 comments on commit 3319ef7

Please sign in to comment.