Reduce triple call to single call, improve video loading, fix embed play button being off-center (#546)

Lots of optimizations and cleanup for the player. If we run into any strange issues, can revert.
This commit is contained in:
mayeaux 2022-01-06 20:28:27 +01:00 committed by GitHub
parent 0a986b1603
commit 58bdcbd1ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 302 additions and 190 deletions

View file

@ -2,3 +2,5 @@ node_modules/*
./node_modules/**
**/node_modules/**
web/dist/**
ui/component/viewers/videoViewer/internal/plugins/canAutoplay.js

View file

@ -63,7 +63,7 @@ type Analytics = {
tagFollowEvent: (string, boolean, ?string) => void,
playerLoadedEvent: (string, ?boolean) => void,
playerVideoStartedEvent: (?boolean) => void,
videoStartEvent: (string, number, string, number, string, any, number) => void,
videoStartEvent: (?string, number, string, ?number, string, any, ?number) => void,
videoIsPlaying: (boolean, any) => void,
videoBufferEvent: (
StreamClaim,

View file

@ -9,9 +9,9 @@ import * as COLLECTIONS_CONSTS from 'constants/collections';
import {
doChangeVolume,
doChangeMute,
doAnalyticsView,
doAnalyticsBuffer,
doAnaltyicsPurchaseEvent,
doAnalyticsView,
} from 'redux/actions/app';
import { selectVolume, selectMute } from 'redux/selectors/app';
import { savePosition, clearPosition, doPlayUri, doSetPlayingUri } from 'redux/actions/content';
@ -76,9 +76,7 @@ const perform = (dispatch) => ({
savePosition: (uri, position) => dispatch(savePosition(uri, position)),
clearPosition: (uri) => dispatch(clearPosition(uri)),
changeMute: (muted) => dispatch(doChangeMute(muted)),
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
doAnalyticsBuffer: (uri, bufferData) => dispatch(doAnalyticsBuffer(uri, bufferData)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
toggleVideoTheaterMode: () => dispatch(toggleVideoTheaterMode()),
toggleAutoplayNext: () => dispatch(toggleAutoplayNext()),
setVideoPlaybackRate: (rate) => dispatch(doSetClientSetting(SETTINGS.VIDEO_PLAYBACK_RATE, rate)),
@ -95,6 +93,8 @@ const perform = (dispatch) => ({
),
dispatch(doSetPlayingUri({ uri, collectionId }))
),
doAnalyticsView: (uri, timeToStart) => dispatch(doAnalyticsView(uri, timeToStart)),
claimRewards: () => dispatch(doClaimEligiblePurchaseRewards()),
});
export default withRouter(connect(select, perform)(VideoViewer));

View file

@ -0,0 +1,23 @@
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.owns=function(a,c){return Object.prototype.hasOwnProperty.call(a,c)};$jscomp.ASSUME_ES5=!1;$jscomp.ASSUME_NO_NATIVE_MAP=!1;$jscomp.ASSUME_NO_NATIVE_SET=!1;$jscomp.defineProperty=$jscomp.ASSUME_ES5||"function"==typeof Object.defineProperties?Object.defineProperty:function(a,c,e){a!=Array.prototype&&a!=Object.prototype&&(a[c]=e.value)};
$jscomp.getGlobal=function(a){return"undefined"!=typeof window&&window===a?a:"undefined"!=typeof global&&null!=global?global:a};$jscomp.global=$jscomp.getGlobal(this);$jscomp.polyfill=function(a,c,e,f){if(c){e=$jscomp.global;a=a.split(".");for(f=0;f<a.length-1;f++){var b=a[f];b in e||(e[b]={});e=e[b]}a=a[a.length-1];f=e[a];c=c(f);c!=f&&null!=c&&$jscomp.defineProperty(e,a,{configurable:!0,writable:!0,value:c})}};
$jscomp.polyfill("Object.assign",function(a){return a?a:function(a,e){for(var c=1;c<arguments.length;c++){var b=arguments[c];if(b)for(var g in b)$jscomp.owns(b,g)&&(a[g]=b[g])}return a}},"es6","es3");$jscomp.SYMBOL_PREFIX="jscomp_symbol_";$jscomp.initSymbol=function(){$jscomp.initSymbol=function(){};$jscomp.global.Symbol||($jscomp.global.Symbol=$jscomp.Symbol)};$jscomp.Symbol=function(){var a=0;return function(c){return $jscomp.SYMBOL_PREFIX+(c||"")+a++}}();
$jscomp.initSymbolIterator=function(){$jscomp.initSymbol();var a=$jscomp.global.Symbol.iterator;a||(a=$jscomp.global.Symbol.iterator=$jscomp.global.Symbol("iterator"));"function"!=typeof Array.prototype[a]&&$jscomp.defineProperty(Array.prototype,a,{configurable:!0,writable:!0,value:function(){return $jscomp.arrayIterator(this)}});$jscomp.initSymbolIterator=function(){}};$jscomp.arrayIterator=function(a){var c=0;return $jscomp.iteratorPrototype(function(){return c<a.length?{done:!1,value:a[c++]}:{done:!0}})};
$jscomp.iteratorPrototype=function(a){$jscomp.initSymbolIterator();a={next:a};a[$jscomp.global.Symbol.iterator]=function(){return this};return a};$jscomp.makeIterator=function(a){$jscomp.initSymbolIterator();var c=a[Symbol.iterator];return c?c.call(a):$jscomp.arrayIterator(a)};$jscomp.FORCE_POLYFILL_PROMISE=!1;
$jscomp.polyfill("Promise",function(a){function c(){this.batch_=null}function e(d){return d instanceof b?d:new b(function(a,b){a(d)})}if(a&&!$jscomp.FORCE_POLYFILL_PROMISE)return a;c.prototype.asyncExecute=function(d){null==this.batch_&&(this.batch_=[],this.asyncExecuteBatch_());this.batch_.push(d);return this};c.prototype.asyncExecuteBatch_=function(){var d=this;this.asyncExecuteFunction(function(){d.executeBatch_()})};var f=$jscomp.global.setTimeout;c.prototype.asyncExecuteFunction=function(d){f(d,
0)};c.prototype.executeBatch_=function(){for(;this.batch_&&this.batch_.length;){var d=this.batch_;this.batch_=[];for(var a=0;a<d.length;++a){var b=d[a];delete d[a];try{b()}catch(h){this.asyncThrow_(h)}}}this.batch_=null};c.prototype.asyncThrow_=function(d){this.asyncExecuteFunction(function(){throw d;})};var b=function(d){this.state_=0;this.result_=void 0;this.onSettledCallbacks_=[];var a=this.createResolveAndReject_();try{d(a.resolve,a.reject)}catch(l){a.reject(l)}};b.prototype.createResolveAndReject_=
function(){function a(a){return function(d){c||(c=!0,a.call(b,d))}}var b=this,c=!1;return{resolve:a(this.resolveTo_),reject:a(this.reject_)}};b.prototype.resolveTo_=function(a){if(a===this)this.reject_(new TypeError("A Promise cannot resolve to itself"));else if(a instanceof b)this.settleSameAsPromise_(a);else{a:switch(typeof a){case "object":var d=null!=a;break a;case "function":d=!0;break a;default:d=!1}d?this.resolveToNonPromiseObj_(a):this.fulfill_(a)}};b.prototype.resolveToNonPromiseObj_=function(a){var b=
void 0;try{b=a.then}catch(l){this.reject_(l);return}"function"==typeof b?this.settleSameAsThenable_(b,a):this.fulfill_(a)};b.prototype.reject_=function(a){this.settle_(2,a)};b.prototype.fulfill_=function(a){this.settle_(1,a)};b.prototype.settle_=function(a,b){if(0!=this.state_)throw Error("Cannot settle("+a+", "+b|"): Promise already settled in state"+this.state_);this.state_=a;this.result_=b;this.executeOnSettledCallbacks_()};b.prototype.executeOnSettledCallbacks_=function(){if(null!=this.onSettledCallbacks_){for(var a=
this.onSettledCallbacks_,b=0;b<a.length;++b)a[b].call(),a[b]=null;this.onSettledCallbacks_=null}};var g=new c;b.prototype.settleSameAsPromise_=function(a){var b=this.createResolveAndReject_();a.callWhenSettled_(b.resolve,b.reject)};b.prototype.settleSameAsThenable_=function(a,b){var c=this.createResolveAndReject_();try{a.call(b,c.resolve,c.reject)}catch(h){c.reject(h)}};b.prototype.then=function(a,c){function d(a,b){return"function"==typeof a?function(b){try{h(a(b))}catch(m){e(m)}}:b}var h,e,g=new b(function(a,
b){h=a;e=b});this.callWhenSettled_(d(a,h),d(c,e));return g};b.prototype.catch=function(a){return this.then(void 0,a)};b.prototype.callWhenSettled_=function(a,b){function c(){switch(d.state_){case 1:a(d.result_);break;case 2:b(d.result_);break;default:throw Error("Unexpected state: "+d.state_);}}var d=this;null==this.onSettledCallbacks_?g.asyncExecute(c):this.onSettledCallbacks_.push(function(){g.asyncExecute(c)})};b.resolve=e;b.reject=function(a){return new b(function(b,c){c(a)})};b.race=function(a){return new b(function(b,
c){for(var d=$jscomp.makeIterator(a),g=d.next();!g.done;g=d.next())e(g.value).callWhenSettled_(b,c)})};b.all=function(a){var c=$jscomp.makeIterator(a),d=c.next();return d.done?e([]):new b(function(a,b){function g(b){return function(c){f[b]=c;h--;0==h&&a(f)}}var f=[],h=0;do f.push(void 0),h++,e(d.value).callWhenSettled_(g(f.length-1),b),d=c.next();while(!d.done)})};return b},"es6","es3");
(function(a,c){"object"===typeof exports&&"undefined"!==typeof module?module.exports=c():"function"===typeof define&&define.amd?define(c):a.canAutoplay=c()})(this,function(){function a(a){return Object.assign({muted:!1,timeout:250,inline:!1},a)}function c(a,c){var b=a.muted,e=a.timeout;a=a.inline;c=c();var f=c.element;c=c.source;var h=void 0,g=void 0,k=void 0;f.muted=b;!0===b&&f.setAttribute("muted","muted");!0===a&&f.setAttribute("playsinline","playsinline");f.src=c;return new Promise(function(a){h=
f.play();g=setTimeout(function(){k(!1,Error("Timeout "+e+" ms has been reached"))},e);k=function(b){var c=1<arguments.length&&void 0!==arguments[1]?arguments[1]:null;clearTimeout(g);a({result:b,error:c})};void 0!==h?h.then(function(){return k(!0)}).catch(function(a){return k(!1,a)}):k(!0)})}var e=new Blob([new Uint8Array([255,227,24,196,0,0,0,3,72,1,64,0,0,4,132,16,31,227,192,225,76,255,67,12,255,221,27,255,228,97,73,63,255,195,131,69,192,232,223,255,255,207,102,239,255,255,255,101,158,206,70,20,
59,255,254,95,70,149,66,4,16,128,0,2,2,32,240,138,255,36,106,183,255,227,24,196,59,11,34,62,80,49,135,40,0,253,29,191,209,200,141,71,7,255,252,152,74,15,130,33,185,6,63,255,252,195,70,203,86,53,15,255,255,247,103,76,121,64,32,47,255,34,227,194,209,138,76,65,77,69,51,46,57,55,170,170,170,170,170,170,170,170,170,170,255,227,24,196,73,13,153,210,100,81,135,56,0,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,
170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170,170])],{type:"audio/mpeg"}),f=new Blob([new Uint8Array([0,0,0,28,102,116,121,112,105,115,111,109,0,0,2,0,105,115,111,109,105,115,111,50,109,112,52,49,0,0,0,8,102,114,101,101,0,0,2,239,109,100,97,116,33,16,5,32,164,27,255,192,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,55,167,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,33,16,5,32,164,27,255,192,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,55,167,128,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,112,0,0,2,194,109,111,111,118,0,0,0,108,109,118,104,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,232,0,0,0,47,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,236,116,114,97,107,0,0,0,92,116,107,104,100,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,47,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,36,101,100,116,115,0,0,0,28,101,108,115,116,0,0,0,0,0,0,0,1,0,0,0,47,0,0,0,0,0,1,0,0,0,0,1,100,109,100,105,97,0,0,0,32,109,100,104,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,172,68,0,0,8,0,85,196,0,0,0,0,0,45,104,100,108,114,0,
0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0,0,0,1,15,109,105,110,102,0,0,0,16,115,109,104,100,0,0,0,0,0,0,0,0,0,0,0,36,100,105,110,102,0,0,0,28,100,114,101,102,0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1,0,0,0,211,115,116,98,108,0,0,0,103,115,116,115,100,0,0,0,0,0,0,0,1,0,0,0,87,109,112,52,97,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,16,0,0,0,0,172,68,0,0,0,0,0,51,101,115,100,115,0,0,0,0,3,128,128,128,34,0,2,0,4,128,128,128,20,64,21,0,0,0,0,
1,244,0,0,1,243,249,5,128,128,128,2,18,16,6,128,128,128,1,2,0,0,0,24,115,116,116,115,0,0,0,0,0,0,0,1,0,0,0,2,0,0,4,0,0,0,0,28,115,116,115,99,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,2,0,0,0,1,0,0,0,28,115,116,115,122,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,115,0,0,1,116,0,0,0,20,115,116,99,111,0,0,0,0,0,0,0,1,0,0,0,44,0,0,0,98,117,100,116,97,0,0,0,90,109,101,116,97,0,0,0,0,0,0,0,33,104,100,108,114,0,0,0,0,0,0,0,0,109,100,105,114,97,112,112,108,0,0,0,0,0,0,0,0,0,0,0,0,45,105,108,115,116,0,0,0,37,169,116,111,111,0,0,0,
29,100,97,116,97,0,0,0,1,0,0,0,0,76,97,118,102,53,54,46,52,48,46,49,48,49])],{type:"video/mp4"});return{audio:function(b){b=a(b);return c(b,function(){return{element:document.createElement("audio"),source:URL.createObjectURL(e)}})},video:function(b){b=a(b);return c(b,function(){return{element:document.createElement("video"),source:URL.createObjectURL(f)}})}}});

View file

@ -1,5 +1,6 @@
// @flow
import { useEffect } from 'react';
import analytics from 'analytics';
const isDev = process.env.NODE_ENV !== 'production';
@ -25,6 +26,14 @@ const VideoJsEvents = ({
playerRef,
autoplaySetting,
replay,
claimId,
userId,
claimValues,
embedded,
uri,
doAnalyticsView,
claimRewards,
playerServerRef,
}: {
tapToUnmuteRef: any, // DOM element
tapToRetryRef: any, // DOM element
@ -33,7 +42,56 @@ const VideoJsEvents = ({
playerRef: any, // DOM element
autoplaySetting: boolean,
replay: boolean,
claimId: ?string,
userId: ?number,
claimValues: any,
embedded: boolean,
clearPosition: (string) => void,
uri: string,
doAnalyticsView: (string, number) => any,
claimRewards: () => void,
playerServerRef: any
}) => {
/**
* Analytics functionality that is run on first video start
* @param e - event from videojs (from the plugin?)
* @param data - only has secondsToLoad property
*/
function doTrackingFirstPlay(e: Event, data: any) {
// how long until the video starts
let timeToStartVideo = data.secondsToLoad;
analytics.playerVideoStartedEvent(embedded);
// convert bytes to bits, and then divide by seconds
const contentInBits = Number(claimValues.source.size) * 8;
const durationInSeconds = claimValues.video && claimValues.video.duration;
let bitrateAsBitsPerSecond;
if (durationInSeconds) {
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
}
// figure out what server the video is served from and then run start analytic event
// server string such as 'eu-p6'
const playerPoweredBy = playerServerRef.current;
// populates data for watchman, sends prom and matomo event
analytics.videoStartEvent(
claimId,
timeToStartVideo,
playerPoweredBy,
userId,
uri,
this, // pass the player
bitrateAsBitsPerSecond
);
// hit backend to mark a view
doAnalyticsView(uri, timeToStartVideo).then(() => {
claimRewards();
});
}
// Override the player's control text. We override to:
// 1. Add keyboard shortcut to the tool-tip.
// 2. Override videojs' i18n and use our own (don't want to have 2 systems).
@ -93,6 +151,10 @@ const VideoJsEvents = ({
function onInitialPlay() {
const player = playerRef.current;
const bigPlayButton = document.querySelector('.vjs-big-play-button');
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'none');
if (player && (player.muted() || player.volume() === 0)) {
// The css starts as "hidden". We make it visible here without
// re-rendering the whole thing.
@ -223,6 +285,20 @@ const VideoJsEvents = ({
player.on('volumechange', resolveCtrlText);
player.on('volumechange', onVolumeChange);
player.on('error', onError);
// custom tracking plugin, event used for watchman data, and marking view/getting rewards
player.on('tracking:firstplay', doTrackingFirstPlay);
// hide forcing control bar show
player.on('canplaythrough', function() {
setTimeout(function() {
// $FlowFixMe
const vjsControlBar = document.querySelector('.vjs-control-bar');
if (vjsControlBar) vjsControlBar.style.removeProperty('opacity');
}, 1000 * 3); // wait 3 seconds to hit control bar
});
player.on('playing', function() {
// $FlowFixMe
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important');
});
// player.on('ended', onEnded);
}

View file

@ -1,48 +1,9 @@
// @flow
const VideoJsFunctions = ({
source,
sourceType,
videoJsOptions,
isAudio,
}: {
source: string,
sourceType: string,
videoJsOptions: Object,
isAudio: boolean,
}) => {
function detectFileType() {
// $FlowFixMe
return new Promise(async (res, rej) => {
try {
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
// Temp variables to hold results
let finalType = sourceType;
let finalSource = source;
// override type if we receive an .m3u8 (transcoded mp4)
// do we need to check if explicitly redirected
// or is checking extension only a safer method
if (response && response.redirected && response.url && response.url.endsWith('m3u8')) {
finalType = 'application/x-mpegURL';
finalSource = response.url;
}
// Modify video source in options
videoJsOptions.sources = [
{
src: finalSource,
type: finalType,
},
];
return res(videoJsOptions);
} catch (error) {
return rej(error);
}
});
}
// TODO: can remove this function as well
// Create the video DOM element and wrapper
function createVideoPlayerDOM(container: any) {
@ -61,7 +22,6 @@ const VideoJsFunctions = ({
}
return {
detectFileType,
createVideoPlayerDOM,
};
};

View file

@ -4,7 +4,6 @@ import 'videojs-ima'; // loads directly after contrib-ads
import 'video.js/dist/alt/video-js-cdn.min.css';
import './plugins/videojs-mobile-ui/plugin';
import '@silvermine/videojs-chromecast/dist/silvermine-videojs-chromecast.css';
import * as ICONS from 'constants/icons';
import * as OVERLAY from './overlays';
import Button from 'component/button';
@ -22,6 +21,7 @@ import React, { useEffect, useRef, useState } from 'react';
import recsys from './plugins/videojs-recsys/plugin';
// import runAds from './ads';
import videojs from 'video.js';
const canAutoplay = require('./plugins/canAutoplay');
require('@silvermine/videojs-chromecast')(videojs);
@ -73,6 +73,12 @@ type Props = {
playNext: () => void,
playPrevious: () => void,
toggleVideoTheaterMode: () => void,
claimRewards: () => void,
doAnalyticsView: (string, number) => void,
uri: string,
claimValues: any,
clearPosition: (string) => void,
centerPlayButton: () => void,
};
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
@ -83,18 +89,6 @@ const IS_IOS =
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
!window.MSStream;
const VIDEO_JS_OPTIONS = {
preload: 'auto',
playbackRates: videoPlaybackRates,
responsive: true,
controls: true,
html5: {
vhs: {
overrideNative: !videojs.browser.IS_ANY_SAFARI,
},
},
};
if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) {
videojs.registerPlugin('eventTracking', eventTracking);
}
@ -142,6 +136,12 @@ export default React.memo<Props>(function VideoJs(props: Props) {
playNext,
playPrevious,
toggleVideoTheaterMode,
claimValues,
doAnalyticsView,
claimRewards,
uri,
clearPosition,
centerPlayButton,
} = props;
// will later store the videojs player
@ -151,16 +151,46 @@ export default React.memo<Props>(function VideoJs(props: Props) {
const tapToUnmuteRef = useRef();
const tapToRetryRef = useRef();
const playerServerRef = useRef();
// initiate keyboard shortcuts
const { curried_function } = keyboardShorcuts({ toggleVideoTheaterMode, playNext, playPrevious });
const [reload, setReload] = useState('initial');
const { createVideoPlayerDOM } = functions({ isAudio });
const { unmuteAndHideHint, retryVideoAfterFailure, initializeEvents } = events({
tapToUnmuteRef,
tapToRetryRef,
setReload,
videoTheaterMode,
playerRef,
autoplaySetting,
replay,
claimValues,
userId,
claimId,
embedded,
doAnalyticsView,
claimRewards,
uri,
playerServerRef,
clearPosition,
});
const videoJsOptions = {
...VIDEO_JS_OPTIONS,
preload: 'auto',
playbackRates: videoPlaybackRates,
responsive: true,
controls: true,
html5: {
vhs: {
overrideNative: !videojs.browser.IS_ANY_SAFARI,
},
},
autoplay: autoplay,
muted: startMuted,
sources: [{ src: source, type: sourceType }],
poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
plugins: { eventTracking: true, overlay: OVERLAY.OVERLAY_DATA },
// fixes problem of errant CC button showing up on iOS
@ -171,25 +201,14 @@ export default React.memo<Props>(function VideoJs(props: Props) {
requestTitleFn: (src) => title || '',
requestSubtitleFn: (src) => channelName || '',
},
bigPlayButton: embedded, // only show big play button if embedded
};
const { detectFileType, createVideoPlayerDOM } = functions({ source, sourceType, videoJsOptions, isAudio });
const { unmuteAndHideHint, retryVideoAfterFailure, initializeEvents } = events({
tapToUnmuteRef,
tapToRetryRef,
setReload,
videoTheaterMode,
playerRef,
autoplaySetting,
replay,
});
// Initialize video.js
function initializeVideoPlayer(el) {
function initializeVideoPlayer(el, canAutoplayVideo) {
if (!el) return;
const vjs = videojs(el, videoJsOptions, () => {
const vjs = videojs(el, videoJsOptions, async () => {
const player = playerRef.current;
const adapter = new playerjs.VideoJSAdapter(player);
@ -209,6 +228,13 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// Initialize mobile UI.
player.mobileUi();
if (!embedded) {
window.player.bigPlayButton && window.player.bigPlayButton.hide();
} else {
const bigPlayButton = document.querySelector('.vjs-big-play-button');
if (bigPlayButton) bigPlayButton.style.setProperty('display', 'block', 'important');
}
Chromecast.initialize(player);
// Add quality selector to player
@ -228,11 +254,32 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// set playsinline for mobile
player.children_[0].setAttribute('playsinline', '');
if (canAutoplayVideo === true) {
// show waiting spinner as video is loading
player.addClass('vjs-waiting');
// document.querySelector('.vjs-big-play-button').style.setProperty('display', 'none', 'important');
} else {
// $FlowFixMe
document.querySelector('.vjs-big-play-button').style.setProperty('display', 'block', 'important');
}
// center play button
centerPlayButton();
// I think this is a callback function
const videoNode = containerRef.current && containerRef.current.querySelector('video, audio');
onPlayerReady(player, videoNode);
adapter.ready();
// sometimes video doesnt start properly, this addresses the edge case
if (autoplay) {
const videoDiv = window.player.children_[0];
if (videoDiv) {
videoDiv.click();
}
window.player.userActive(true);
}
});
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
@ -245,12 +292,16 @@ export default React.memo<Props>(function VideoJs(props: Props) {
/** instantiate videoJS and dispose of it when done with code **/
// This lifecycle hook is only called once (on mount), or when `isAudio` or `source` changes.
useEffect(() => {
const vjsElement = createVideoPlayerDOM(containerRef.current);
(async function() {
// test if perms to play video are available
let canAutoplayVideo = await canAutoplay.video({ timeout: 2000 });
canAutoplayVideo = canAutoplayVideo.result === true;
const vjsElement = createVideoPlayerDOM(containerRef.current);
// Detect source file type via pre-fetch (async)
detectFileType().then(() => {
// Initialize Video.js
const vjsPlayer = initializeVideoPlayer(vjsElement);
const vjsPlayer = initializeVideoPlayer(vjsElement, canAutoplayVideo);
// Add reference to player to global scope
window.player = vjsPlayer;
@ -259,7 +310,34 @@ export default React.memo<Props>(function VideoJs(props: Props) {
playerRef.current = vjsPlayer;
window.addEventListener('keydown', curried_function(playerRef, containerRef));
});
// $FlowFixMe
document.querySelector('.vjs-control-bar').style.setProperty('opacity', '1', 'important');
// change to m3u8 if applicable
const response = await fetch(source, { method: 'HEAD', cache: 'no-store' });
playerServerRef.current = response.headers.get('x-powered-by');
if (response && response.redirected && response.url && response.url.endsWith('m3u8')) {
// use m3u8 source
// $FlowFixMe
vjsPlayer.src({
type: 'application/x-mpegURL',
src: response.url,
});
} else {
// use original mp4 source
// $FlowFixMe
vjsPlayer.src({
type: sourceType,
src: source,
});
}
// load video once source setup
// $FlowFixMe
vjsPlayer.load();
})();
// Cleanup
return () => {
@ -275,41 +353,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
window.player = undefined;
}
};
}, [isAudio, source]);
// Update video player and reload when source URL changes
useEffect(() => {
// For some reason the video player is responsible for detecting content type this way
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
let finalType = sourceType;
let finalSource = source;
// override type if we receive an .m3u8 (transcoded mp4)
// do we need to check if explicitly redirected
// or is checking extension only a safer method
if (response && response.redirected && response.url && response.url.endsWith('m3u8')) {
finalType = 'application/x-mpegURL';
finalSource = response.url;
}
// Modify video source in options
videoJsOptions.sources = [
{
src: finalSource,
type: finalType,
},
];
// Update player source
const player = playerRef.current;
if (!player) return;
// PR #5570: Temp workaround to avoid double Play button until the next re-architecture.
if (!player.paused()) {
player.bigPlayButton.hide();
}
});
}, [source, reload]);
}, [isAudio, source, reload]);
return (
<div className={classnames('video-js-parent', { 'video-js-parent--ios': IS_IOS })} ref={containerRef}>

View file

@ -14,7 +14,6 @@ import AutoplayCountdown from 'component/autoplayCountdown';
import usePrevious from 'effects/use-previous';
import FileViewerEmbeddedEnded from 'web/component/fileViewerEmbeddedEnded';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import LoadingScreen from 'component/common/loading-screen';
import { addTheaterModeButton } from './internal/theater-mode';
import { addAutoplayNextButton } from './internal/autoplay-next';
import { addPlayNextButton } from './internal/play-next';
@ -28,8 +27,8 @@ import type { HomepageCat } from 'util/buildHomepage';
import debounce from 'util/debounce';
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
const PLAY_TIMEOUT_LIMIT = 2000;
// const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
// const PLAY_TIMEOUT_LIMIT = 2000;
type Props = {
position: number,
@ -45,9 +44,7 @@ type Props = {
uri: string,
autoplayNext: boolean,
autoplayIfEmbedded: boolean,
doAnalyticsView: (string, number) => Promise<any>,
doAnalyticsBuffer: (string, any) => void,
claimRewards: () => void,
savePosition: (string, number) => void,
clearPosition: (string) => void,
toggleVideoTheaterMode: () => void,
@ -65,6 +62,8 @@ type Props = {
previousListUri: string,
videoTheaterMode: boolean,
isMarkdownOrComment: boolean,
doAnalyticsView: (string, number) => void,
claimRewards: () => void,
};
/*
@ -87,8 +86,8 @@ function VideoViewer(props: Props) {
volume,
autoplayNext,
autoplayIfEmbedded,
doAnalyticsView,
doAnalyticsBuffer,
doAnalyticsView,
claimRewards,
savePosition,
clearPosition,
@ -133,7 +132,6 @@ function VideoViewer(props: Props) {
const [adUrl, setAdUrl, isFetchingAd] = useGetAds(approvedVideo, adsEnabled);
/* isLoading was designed to show loading screen on first play press, rather than completely black screen, but
breaks because some browsers (e.g. Firefox) block autoplay but leave the player.play Promise pending */
const [isLoading, setIsLoading] = useState(false);
const [replay, setReplay] = useState(false);
const [videoNode, setVideoNode] = useState();
@ -150,7 +148,6 @@ function VideoViewer(props: Props) {
if (uri && previousUri && uri !== previousUri) {
setShowAutoplayCountdown(false);
setIsEndedEmbed(false);
setIsLoading(false);
}
}, [uri, previousUri]);
@ -170,47 +167,6 @@ function VideoViewer(props: Props) {
});
}
/**
* Analytics functionality that is run on first video start
* @param e - event from videojs (from the plugin?)
* @param data - only has secondsToLoad property
*/
function doTrackingFirstPlay(e: Event, data: any) {
// how long until the video starts
let timeToStartVideo = data.secondsToLoad;
analytics.playerVideoStartedEvent(embedded);
// convert bytes to bits, and then divide by seconds
const contentInBits = Number(claim.value.source.size) * 8;
const durationInSeconds = claim.value.video && claim.value.video.duration;
let bitrateAsBitsPerSecond;
if (durationInSeconds) {
bitrateAsBitsPerSecond = Math.round(contentInBits / durationInSeconds);
}
// figure out what server the video is served from and then run start analytic event
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
// server string such as 'eu-p6'
let playerPoweredBy = response.headers.get('x-powered-by') || '';
// populates data for watchman, sends prom and matomo event
analytics.videoStartEvent(
claimId,
timeToStartVideo,
playerPoweredBy,
userId,
claim.canonical_url,
this,
bitrateAsBitsPerSecond
);
});
// hit backend to mark a view
doAnalyticsView(uri, timeToStartVideo).then(() => {
claimRewards();
});
}
const doPlay = useCallback(
(playUri) => {
setDoNavigate(false);
@ -292,7 +248,6 @@ function VideoViewer(props: Props) {
// MORE ON PLAY STUFF
function onPlay(player) {
setEnded(false);
setIsLoading(false);
setIsPlaying(true);
setShowAutoplayCountdown(false);
setIsEndedEmbed(false);
@ -334,6 +289,20 @@ function VideoViewer(props: Props) {
setEnded(true);
};
function centerPlayButton() {
// center play button
const playBT = document.getElementsByClassName('vjs-big-play-button')[0];
const videoDiv = window.player.children_[0];
const controlBar = document.getElementsByClassName('vjs-control-bar')[0];
const leftWidth = (videoDiv.offsetWidth - playBT.offsetWidth) / 2 + 'px';
const availableHeight = videoDiv.offsetHeight - controlBar.offsetHeight;
const topHeight = (availableHeight - playBT.offsetHeight) / 2 + 3 + 'px';
playBT.style.top = topHeight;
playBT.style.left = leftWidth;
playBT.style.margin = '0';
}
const onPlayerReady = useCallback((player: Player, videoNode: any) => {
if (!embedded) {
setVideoNode(videoNode);
@ -351,27 +320,40 @@ function VideoViewer(props: Props) {
}
}
const shouldPlay = !embedded || autoplayIfEmbedded;
// https://blog.videojs.com/autoplay-best-practices-with-video-js/#Programmatic-Autoplay-and-Success-Failure-Detection
if (shouldPlay) {
const playPromise = player.play();
const timeoutPromise = new Promise((resolve, reject) =>
setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
);
Promise.race([playPromise, timeoutPromise]).catch((error) => {
if (typeof error === 'object' && error.name && error.name === 'NotAllowedError') {
if (player.autoplay() && !player.muted()) {
// player.muted(true);
// another version had player.play()
}
}
setIsLoading(false);
setIsPlaying(false);
});
}
setIsLoading(shouldPlay); // if we are here outside of an embed, we're playing
// currently not being used, but leaving for time being
// const shouldPlay = !embedded || autoplayIfEmbedded;
// // https://blog.videojs.com/autoplay-best-practices-with-video-js/#Programmatic-Autoplay-and-Success-Failure-Detection
// if (shouldPlay) {
// const playPromise = player.play();
//
// const timeoutPromise = new Promise((resolve, reject) =>
// setTimeout(() => reject(PLAY_TIMEOUT_ERROR), PLAY_TIMEOUT_LIMIT)
// );
//
// // if user hasn't interacted with document, mute video and play it
// Promise.race([playPromise, timeoutPromise]).catch((error) => {
// console.log(error);
// console.log(playPromise);
//
// const noPermissionError = typeof error === 'object' && error.name && error.name === 'NotAllowedError';
// const isATimeoutError = error === PLAY_TIMEOUT_ERROR;
//
// if (noPermissionError) {
// // if (player.paused()) {
// // document.querySelector('.vjs-big-play-button').style.setProperty('display', 'block', 'important');
// // }
//
// centerPlayButton();
//
// // to turn muted autoplay on
// // if (player.autoplay() && !player.muted()) {
// // player.muted(true);
// // player.play();
// // }
// }
// setIsPlaying(false);
// });
// }
// PR: #5535
// Move the restoration to a later `loadedmetadata` phase to counter the
@ -382,8 +364,6 @@ function VideoViewer(props: Props) {
// used for tracking buffering for watchman
player.on('tracking:buffered', doTrackingBuffered);
// first play tracking, used for initializing the watchman api
player.on('tracking:firstplay', doTrackingFirstPlay);
player.on('ended', () => setEnded(true));
player.on('play', onPlay);
player.on('pause', (event) => onPause(event, player));
@ -433,8 +413,6 @@ function VideoViewer(props: Props) {
)}
{isEndedEmbed && <FileViewerEmbeddedEnded uri={uri} />}
{embedded && !isEndedEmbed && <FileViewerEmbeddedTitle uri={uri} />}
{/* disable this loading behavior because it breaks when player.play() promise hangs */}
{isLoading && <LoadingScreen status={__('Loading')} />}
{!isFetchingAd && adUrl && (
<>
@ -489,6 +467,12 @@ function VideoViewer(props: Props) {
playNext={doPlayNext}
playPrevious={doPlayPrevious}
embedded={embedded}
claimValues={claim.value}
doAnalyticsView={doAnalyticsView}
claimRewards={claimRewards}
uri={uri}
clearPosition={clearPosition}
centerPlayButton={centerPlayButton}
/>
</div>
);

View file

@ -20,11 +20,16 @@
border-bottom: 1px solid var(--color-border);
padding: var(--spacing-m);
background-color: var(--color-ads-background);
display: flex;
//display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
.ad__container {
width: 358px;
height: 201px;
}
> div,
ins {
width: 100%;
@ -51,6 +56,7 @@
}
.ads__claim-text {
margin-top: 5px;
display: flex;
flex-direction: column;
justify-content: center;

View file

@ -225,3 +225,18 @@ $control-bar-icon-size: 0.8rem;
}
}
}
// larger than default spinner for all but smallest devices
@media (min-width:680px) {
.vjs-loading-spinner {
border-radius: 100px;
height: 75px;
width: 75px;
margin: -49px 0 0 -37px;
}
}
// TODO: make sure there's no bad side effects of this
button.vjs-big-play-button {
display: none !important;
}

View file

@ -124,7 +124,9 @@ function Ads(props: Props) {
// ad shown in the related videos area
const videoAd = (
<div className="ads__claim-item">
<div id={tagNameToUse} className="ads__injected-video" style={{ display: 'none' }} />
<div className="ad__container">
<div id={tagNameToUse} className="ads__injected-video" style={{ display: 'none' }} />
</div>
<div
className={classnames('ads__claim-text', {
'ads__claim-text--small': small,