Signal-iOS/SignalMessaging/Views/LoopingVideoView.swift
Michelle Linington e664f52ff4 A collection of fixes for mp4 gifs
A couple of bugs have cropped up with viewing of mp4 gifs. This hasn't
been enabled internally, so I'm guessing some bugs have snuck through as
other changes have been made in the vicinity. This addresses most of
them:
- Better layout in the media detail viewer
- More accurate descriptions
- Fix for failed thumbnailing
- Fix for keyboard tracking bug with reduce motion enabled
2021-08-10 10:57:04 -03:00

190 lines
5.9 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import YYImage
import AVKit
import PromiseKit
/// Model object for a looping video asset
/// Any LoopingVideoViews playing this instance will all be kept in sync
@objc
public class LoopingVideo: NSObject {
fileprivate let assetPromise: Guarantee<AVAsset?>
fileprivate var asset: AVAsset? { assetPromise.value.flatMap { $0 } }
@objc
public init?(url: URL) {
guard OWSMediaUtils.isVideoOfValidContentTypeAndSize(path: url.path) else {
return nil
}
assetPromise = firstly(on: .global(qos: .userInitiated)) { AVAsset(url: url) }
super.init()
}
func createPlayerItem() -> AVPlayerItem? {
guard let asset = asset else { return nil }
let item = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["tracks"])
return OWSMediaUtils.isValidVideo(asset: asset) ? item : nil
}
deinit {
asset?.cancelLoading()
}
}
private class LoopingVideoPlayer: AVPlayer {
override init() {
super.init()
sharedInit()
}
override init(url: URL) {
super.init(url: url)
sharedInit()
}
override init(playerItem item: AVPlayerItem?) {
super.init(playerItem: item)
sharedInit()
}
private func sharedInit() {
if let item = currentItem {
NotificationCenter.default.addObserver(
self,
selector: #selector(self.playerItemDidPlayToCompletion(_:)),
name: .AVPlayerItemDidPlayToEndTime,
object: item)
}
isMuted = true
allowsExternalPlayback = true
if #available(iOS 12, *) {
preventsDisplaySleepDuringVideoPlayback = false
}
}
override func replaceCurrentItem(with newItem: AVPlayerItem?) {
readyStatusObserver = nil
if let oldItem = currentItem {
NotificationCenter.default.removeObserver(
self,
name: .AVPlayerItemDidPlayToEndTime,
object: oldItem)
oldItem.cancelPendingSeeks()
}
super.replaceCurrentItem(with: newItem)
NotificationCenter.default.addObserver(
self,
selector: #selector(self.playerItemDidPlayToCompletion(_:)),
name: .AVPlayerItemDidPlayToEndTime,
object: newItem)
}
@objc private func playerItemDidPlayToCompletion(_ notification: NSNotification) {
guard (notification.object as AnyObject) === currentItem else { return }
seek(to: .zero)
play()
}
private var readyStatusObserver: NSKeyValueObservation?
override public func play() {
// Don't bother if we're already playing, or we don't have an item
guard let item = currentItem, rate == 0 else { return }
if item.status == .readyToPlay {
readyStatusObserver = nil
super.play()
} else if readyStatusObserver == nil {
// We're not ready to play, set up an observer to play when ready
readyStatusObserver = item.observe(\.status) { [weak self] _, _ in
guard let self = self, item === self.currentItem else { return }
if item.status == .readyToPlay {
self.play()
}
}
}
}
}
@objc
public class LoopingVideoView: UIView {
private let player = LoopingVideoPlayer()
@objc
public var video: LoopingVideo? {
didSet {
guard video !== oldValue else { return }
player.replaceCurrentItem(with: nil)
invalidateIntrinsicContentSize()
if let assetPromise = video?.assetPromise {
assetPromise.done(on: .global(qos: .userInitiated)) { asset in
guard asset === self.video?.asset else { return }
if let asset = asset {
let playerItem = AVPlayerItem(asset: asset, automaticallyLoadedAssetKeys: ["tracks"])
self.player.replaceCurrentItem(with: playerItem)
self.player.play()
DispatchQueue.main.async { self.invalidateIntrinsicContentSize() }
}
}
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
playerLayer.player = player
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public static var layerClass: AnyClass { AVPlayerLayer.self }
private var playerLayer: AVPlayerLayer {
layer as? AVPlayerLayer ?? {
owsFailDebug("Unexpected player type")
return AVPlayerLayer()
}()
}
override public var contentMode: UIView.ContentMode {
didSet {
switch contentMode {
case .scaleAspectFill: playerLayer.videoGravity = .resizeAspectFill
case .scaleToFill: playerLayer.videoGravity = .resize
case .scaleAspectFit: playerLayer.videoGravity = .resizeAspect
default: playerLayer.videoGravity = .resizeAspect
}
}
}
override public var intrinsicContentSize: CGSize {
guard let asset = video?.asset else {
// If we have an outstanding promise, invalidate the size once it's complete
// If there isn't, -noIntrinsicMetric is valid
if video?.assetPromise.isPending == true {
video?.assetPromise.done { _ in self.invalidateIntrinsicContentSize() }
}
return CGSize(square: UIView.noIntrinsicMetric)
}
// Tracks will always be loaded by LoopingVideo
return asset.tracks(withMediaType: .video)
.map { $0.naturalSize }
.reduce(.zero) {
CGSize(width: max($0.width, $1.width),
height: max($0.height, $1.height))
}
}
}