Add playback rate control for Youtube & Vimeo

This commit is contained in:
Ilya Laktyushin 2021-07-22 21:33:32 +03:00
parent 4433eefe15
commit 8c69553891
12 changed files with 168 additions and 76 deletions

View File

@ -21,6 +21,7 @@ import TextSelectionNode
import UrlEscaping
import UndoUI
import ManagedAnimationNode
import TelegramUniversalVideoContent
private let deleteImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionTrash"), color: .white)
private let actionImage = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionForward"), color: .white)
@ -593,6 +594,14 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, UIScroll
break
}
}
} else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content {
let type = webEmbedType(content: content)
switch type {
case .youtube, .vimeo:
canFullscreen = true
default:
break
}
}
}

View File

@ -774,6 +774,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var forceEnablePiP = false
var forceEnableUserInteraction = false
var isAnimated = false
var isEnhancedWebPlayer = false
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
self.videoFramePreview = MediaPlayerFramePreview(postbox: item.context.account.postbox, fileReference: content.fileReference)
@ -783,9 +784,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let type = webEmbedType(content: content.webpageContent)
switch type {
case .youtube:
isEnhancedWebPlayer = true
forceEnableUserInteraction = true
disablePictureInPicture = !(item.configuration?.youtubePictureInPictureEnabled ?? false)
self.videoFramePreview = YoutubeEmbedFramePreview(context: item.context, content: content)
case .vimeo:
isEnhancedWebPlayer = true
case .iframe:
disablePlayerControls = true
default:
@ -1122,7 +1126,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
if !isWebpage, let file = file, !file.isAnimated {
var hasMoreButton = false
if isEnhancedWebPlayer {
hasMoreButton = true
} else if !isWebpage, let file = file, !file.isAnimated {
hasMoreButton = true
}
if hasMoreButton {
let moreMenuItem = UIBarButtonItem(customDisplayNode: self.moreBarButton)!
barButtonItems.append(moreMenuItem)
}
@ -1316,6 +1327,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if let time = item.timecode {
seek = .timecode(time)
}
playbackRate = item.playbackRate
}
}
videoNode.setBaseRate(playbackRate)
@ -1922,7 +1934,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
private func contentInfo() -> (message: Message, file: TelegramMediaFile, isWebpage: Bool)? {
private func contentInfo() -> (message: Message, file: TelegramMediaFile?, isWebpage: Bool)? {
guard let item = self.item else {
return nil
}
@ -1933,16 +1945,15 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
if let m = m as? TelegramMediaFile, m.isVideo {
file = m
break
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content, let f = content.file, f.isVideo {
file = f
} else if let m = m as? TelegramMediaWebpage, case let .Loaded(content) = m.content {
if let f = content.file, f.isVideo {
file = f
}
isWebpage = true
break
}
}
if let file = file {
return (message, file, isWebpage)
}
return (message, file, isWebpage)
}
return nil
}
@ -2041,7 +2052,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
c.setItems(strongSelf.contextMenuSpeedItems())
})))
if let (message, file, isWebpage) = strongSelf.contentInfo(), !isWebpage {
if let (message, maybeFile, isWebpage) = strongSelf.contentInfo(), let file = maybeFile, !isWebpage {
items.append(.action(ContextMenuActionItem(text: "Save to Gallery", icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Download"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in
f(.default)

View File

@ -282,17 +282,7 @@
} completion:nil];
};
[self addSubview:_zoomView];
_flashControl.becameActive = ^
{
__strong TGCameraMainPhoneView *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (strongSelf->_modeControl.cameraMode == PGCameraModeVideo)
[strongSelf->_timecodeView setHidden:true animated:true];
};
_flashControl.modeChanged = ^(PGCameraFlashMode mode)
{
__strong TGCameraMainPhoneView *strongSelf = weakSelf;
@ -301,9 +291,6 @@
if (strongSelf.flashModeChanged != nil)
strongSelf.flashModeChanged(mode);
if (strongSelf->_modeControl.cameraMode == PGCameraModeVideo)
[strongSelf->_timecodeView setHidden:false animated:true];
};
_modeControl.modeChanged = ^(PGCameraMode mode, PGCameraMode previousMode)

View File

@ -9,14 +9,10 @@
</head>
<body>
<div class="container">
<iframe id="player" src="https://player.vimeo.com/video/%@?api=1&badge=0&byline=0&portrait=0&title=0&player_id=player" width="100%" height="100%" frameborder="0"></iframe>
<iframe id="player" src="https://player.vimeo.com/video/%@?badge=0&byline=0&portrait=0&title=0" width="100%" height="100%" frameborder="0"></iframe>
</div>
<script src="https://player.vimeo.com/api/player.js"></script>
<script>
var Froogaloop=function(){function e(a){return new e.fn.init(a)}function g(a,c,b){if(!b.contentWindow.postMessage)return!1;a=JSON.stringify({method:a,value:c});b.contentWindow.postMessage(a,h)}function l(a){var c,b;try{c=JSON.parse(a.data),b=c.event||c.method}catch(e){}"ready"!=b||k||(k=!0);if(!/^https?:\/\/player.vimeo.com/.test(a.origin))return!1;"*"===h&&(h=a.origin);a=c.value;var m=c.data,f=""===f?null:c.player_id;c=f?d[f][b]:d[b];b=[];if(!c)return!1;void 0!==a&&b.push(a);m&&b.push(m);f&&b.push(f);
return 0<b.length?c.apply(null,b):c.call()}function n(a,c,b){b?(d[b]||(d[b]={}),d[b][a]=c):d[a]=c}var d={},k=!1,h="*";e.fn=e.prototype={element:null,init:function(a){"string"===typeof a&&(a=document.getElementById(a));this.element=a;return this},api:function(a,c){if(!this.element||!a)return!1;var b=this.element,d=""!==b.id?b.id:null,e=c&&c.constructor&&c.call&&c.apply?null:c,f=c&&c.constructor&&c.call&&c.apply?c:null;f&&n(a,f,d);g(a,e,b);return this},addEvent:function(a,c){if(!this.element)return!1;
var b=this.element,d=""!==b.id?b.id:null;n(a,c,d);"ready"!=a?g("addEventListener",a,b):"ready"==a&&k&&c.call(null,d);return this},removeEvent:function(a){if(!this.element)return!1;var c=this.element,b=""!==c.id?c.id:null;a:{if(b&&d[b]){if(!d[b][a]){b=!1;break a}d[b][a]=null}else{if(!d[a]){b=!1;break a}d[a]=null}b=!0}"ready"!=a&&b&&g("removeEventListener",a,c)}};e.fn.init.prototype=e.fn;window.addEventListener?window.addEventListener("message",l,!1):window.attachEvent("onmessage",l);return window.Froogaloop=
window.$f=e}();
var iframe;
var player;
function invoke(command) {
@ -26,7 +22,7 @@
var played = false;
function play() {
if (played) {
player.api("play");
player.play();
} else {
invoke("autoplay");
played = true;
@ -34,44 +30,46 @@
}
function pause() {
player.api("pause");
player.pause();
}
function seek(timestamp) {
player.api("seekTo", timestamp);
player.setCurrentTime(timestamp)
}
function setRate(rate) {
player.setPlaybackRate(rate)
}
(function() {
var playbackState = 0;
var playbackState = 1;
var duration = 0.0;
var position = 0.0;
var downloadProgress = 0.0;
iframe = document.querySelectorAll("iframe")[0];
player = $f(iframe);
function updateState() {
window.location.href = "embed://onState?playback=" + playbackState + "&position=" + position + "&duration=" + duration + "&download=" + downloadProgress;
}
player.addEvent("ready", function(player_id) {
window.location.href = "embed://onReady?data=" + player_id;
player.addEvent("play", onPlay);
player.addEvent("pause", onPause);
player.addEvent("finish", onFinish);
player.addEvent("playProgress", onPlayProgress);
player.addEvent("loadProgress", onLoadProgress);
window.setInterval(updateState, 500);
invoke("initialize");
if (%@) {
invoke("autoplay");
}
player = new Vimeo.Player(iframe);
player.getCurrentTime().then(function(seconds) {
position = seconds;
});
player.getDuration().then(function(seconds) {
duration = seconds;
});
function updateState() {
player.getPaused().then(function(paused) {
playbackState = paused ? 0 : 1;
});
player.getCurrentTime().then(function(seconds) {
position = seconds;
});
player.getDuration().then(function(seconds) {
duration = seconds;
});
window.location.href = "embed://onState?playback=" + playbackState + "&position=" + position + "&duration=" + duration + "&download=" + downloadProgress;
invoke("initialize");
}
function onPlay(data) {
playbackState = 1;
updateState();
@ -95,6 +93,16 @@
function onLoadProgress(data) {
downloadProgress = data.percent;
}
player.on('play', onPlay);
player.on('pause', onPause);
player.on("ended", onFinish);
window.setInterval(updateState, 500);
if (%@) {
invoke("autoplay");
}
})();
</script>
</body>

View File

@ -4,17 +4,17 @@ function initialize() {
controls.style.display = "none";
}
var sidedock = document.getElementsByClassName("sidedock")[0];
var sidedock = document.getElementsByClassName("vp-sidedock")[0];
if (sidedock != null) {
sidedock.style.display = "none";
}
var video = document.getElementsByTagName("video")[0];
if (video != null) {
video.setAttribute("webkit-playsinline", "");
video.setAttribute("playsinline", "");
video.webkitEnterFullscreen = undefined;
}
// var video = document.getElementsByTagName("video")[0];
// if (video != null) {
// video.setAttribute("webkit-playsinline", "");
// video.setAttribute("playsinline", "");
// video.webkitEnterFullscreen = undefined;
// }
}
function eventFire(el, etype){

View File

@ -60,6 +60,10 @@
player.seekTo(timestamp, true);
}
function setRate(rate) {
player.setPlaybackRate(rate);
}
function updateState() {
window.location.href = "embed://onState?failed=" + failed + "&playback=" + playbackState + "&position=" + position + "&duration=" + duration + "&download=" + downloadProgress + '&quality=' + quality + '&availableQualities=' + availableQualities + '&storyboard=' + storyboardSpec;
}

View File

@ -71,6 +71,9 @@ final class GenericEmbedImplementation: WebEmbedImplementation {
func seek(timestamp: Double) {
}
func setBaseRate(_ baseRate: Double) {
}
func pageReady() {
self.status = MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .playing, soundEnabled: true)
self.updateStatus?(self.status)

View File

@ -88,6 +88,9 @@ final class TwitchEmbedImplementation: WebEmbedImplementation {
func seek(timestamp: Double) {
}
func setBaseRate(_ baseRate: Double) {
}
func pageReady() {
// Queue.mainQueue().after(delay: 0.5) {
// if let onPlaybackStarted = self.onPlaybackStarted {

View File

@ -88,6 +88,7 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
private let videoId: String
private let timestamp: Int
private var baseRate: Double = 1.0
private var status : MediaPlayerStatus
private var ready: Bool = false
@ -160,12 +161,30 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
eval("seek(\(timestamp));", nil)
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: timestamp, baseRate: 1.0, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: self.status.soundEnabled)
if let updateStatus = self.updateStatus {
updateStatus(self.status)
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: timestamp, baseRate: self.status.baseRate, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: self.status.soundEnabled)
self.updateStatus?(self.status)
self.ignorePosition = 2
}
func setBaseRate(_ baseRate: Double) {
var baseRate = baseRate
if baseRate < 0.5 {
baseRate = 0.5
}
if baseRate > 2.0 {
baseRate = 2.0
}
if !self.ready {
self.baseRate = baseRate
}
self.ignorePosition = 2
if let eval = self.evalImpl {
eval("setRate(\(baseRate));", nil)
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: baseRate, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: true)
self.updateStatus?(self.status)
}
func pageReady() {
@ -210,6 +229,11 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
}
}
if !self.ready {
self.ready = true
self.play()
}
if let updateStatus = self.updateStatus, let playback = playback, let duration = duration {
let playbackStatus: MediaPlayerPlaybackStatus
switch playback {
@ -226,10 +250,12 @@ final class VimeoEmbedImplementation: WebEmbedImplementation {
if case .playing = playbackStatus, !self.started {
self.started = true
self.onPlaybackStarted?()
Queue.mainQueue().after(0.5) {
self.onPlaybackStarted?()
}
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: 1.0, seekId: self.status.seekId, status: playbackStatus, soundEnabled: true)
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: self.status.baseRate, seekId: self.status.seekId, status: playbackStatus, soundEnabled: true)
updateStatus(self.status)
}
}

View File

@ -14,6 +14,7 @@ protocol WebEmbedImplementation {
func pause()
func togglePlayPause()
func seek(timestamp: Double)
func setBaseRate(_ baseRate: Double)
func pageReady()
func callback(url: URL)
@ -170,6 +171,10 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
self.impl.seek(timestamp: timestamp)
}
func setBaseRate(_ baseRate: Double) {
self.impl.setBaseRate(baseRate)
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
}

View File

@ -172,6 +172,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
}
func setBaseRate(_ baseRate: Double) {
self.playerNode.setBaseRate(baseRate)
}
func addPlaybackCompleted(_ f: @escaping () -> Void) -> Int {

View File

@ -100,6 +100,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
}
private var timestamp: Int
private var baseRate: Double = 1.0
private var ignoreEarlierTimestamps = false
private var status: MediaPlayerStatus
@ -107,6 +108,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
private var started = false
private var ignorePosition: Int?
private var isPlaying = true
private enum PlaybackDelay {
case none
case afterPositionUpdates(count: Int)
@ -186,6 +189,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
return
}
self.isPlaying = true
if let eval = self.evalImpl {
eval("play();", nil)
}
@ -194,6 +199,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
}
func pause() {
self.isPlaying = false
if let eval = self.evalImpl {
eval("pause();", nil)
}
@ -201,9 +207,9 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
func togglePlayPause() {
if case .playing = self.status.status {
pause()
self.pause()
} else {
play()
self.play()
}
}
@ -217,12 +223,32 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
eval("seek(\(timestamp));", nil)
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: timestamp, baseRate: 1.0, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: true)
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: timestamp, baseRate: self.status.baseRate, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: true)
self.updateStatus?(self.status)
self.ignorePosition = 2
}
func setBaseRate(_ baseRate: Double) {
var baseRate = baseRate
if baseRate < 0.5 {
baseRate = 0.5
}
if baseRate > 2.0 {
baseRate = 2.0
}
if !self.ready {
self.baseRate = baseRate
}
if let eval = self.evalImpl {
eval("setRate(\(baseRate));", nil)
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: self.status.duration, dimensions: self.status.dimensions, timestamp: self.status.timestamp, baseRate: baseRate, seekId: self.status.seekId + 1, status: self.status.status, soundEnabled: true)
self.updateStatus?(self.status)
}
func pageReady() {
}
@ -283,6 +309,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
switch playback {
case 0:
if newTimestamp > Double(duration) - 1.0 {
self.isPlaying = false
playbackStatus = .paused
newTimestamp = 0.0
} else {
@ -293,9 +320,9 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
case 2:
playbackStatus = .paused
case 3:
playbackStatus = .buffering(initial: false, whilePlaying: true, progress: 0.0, display: false)
playbackStatus = .buffering(initial: !self.started, whilePlaying: self.isPlaying, progress: 0.0, display: false)
default:
playbackStatus = .buffering(initial: true, whilePlaying: false, progress: 0.0, display: false)
playbackStatus = .buffering(initial: true, whilePlaying: true, progress: 0.0, display: false)
}
if case .playing = playbackStatus, !self.started {
@ -305,7 +332,7 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
self.onPlaybackStarted?()
}
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: 1.0, seekId: self.status.seekId, status: playbackStatus, soundEnabled: true)
self.status = MediaPlayerStatus(generationTimestamp: self.status.generationTimestamp, duration: Double(duration), dimensions: self.status.dimensions, timestamp: newTimestamp, baseRate: self.status.baseRate, seekId: self.status.seekId, status: playbackStatus, soundEnabled: true)
updateStatus(self.status)
}
}
@ -327,12 +354,20 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
self.play()
}
if self.baseRate != 1.0 {
self.setBaseRate(self.baseRate)
}
print("YT ready in \(CFAbsoluteTimeGetCurrent() - self.benchmarkStartTime)")
Queue.mainQueue().async {
self.play()
let delay = self.timestamp > 0 ? 2.8 : 2.0
if self.timestamp > 0 {
self.seek(timestamp: Double(self.timestamp))
self.play()
} else {
self.play()
}
Queue.mainQueue().after(delay, {
if !self.started {
self.play()