Add Native bridge video support (#9303)

This commit is contained in:
Fernando Valverde 2020-07-20 09:22:32 -06:00 committed by GitHub
parent f1b3b5a501
commit 154a3629ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 275 additions and 57 deletions

View file

@ -7,7 +7,8 @@
initializeCommentPreview,
initializeTimeFixer, initializeDashboardSort, initializePWAFunctionality,
initializeEllipsisMenu, initializeArchivedPostFilter, initializeCreditsPage,
initializeUserProfilePage, initializePodcastPlayback, initializeDrawerSliders,
initializeUserProfilePage, initializePodcastPlayback,
initializeVideoPlayback, initializeDrawerSliders,
initializeHeroBannerClose, initializeOnboardingTaskCard, initScrolling,
nextPage:writable, fetching:writable, done:writable, adClicked:writable,
initializePaymentPointers, initializeSpecialNavigationFunctionality, initializeBroadcast,
@ -58,6 +59,7 @@ function callInitializers() {
initializeCreditsPage();
initializeUserProfilePage();
initializePodcastPlayback();
initializeVideoPlayback();
initializeDrawerSliders();
initializeHeroBannerClose();
initializeOnboardingTaskCard();

View file

@ -0,0 +1,195 @@
/**
* This script hunts for video tags and initializes the correct player
* depending on the platform:
* - web: jwplayer
* - iOS/Android: Native player
*
* Once jwplayer is initialized there's no follow up actions to be taken.
* Mobile Native players send back information into the DOM in order to
* interact and update the UI, therefore a MutationObserver is registered.
*/
/* eslint no-use-before-define: 0 */
/* eslint no-param-reassign: 0 */
/* eslint no-useless-escape: 0 */
/* global jwplayer */
function initializeVideoPlayback() {
var nativeBridgeMessage;
var currentTime = '0';
function getById(name) {
return document.getElementById(name);
}
function isNativeIOS() {
return (
navigator.userAgent === 'DEV-Native-ios' &&
window &&
window.webkit &&
window.webkit.messageHandlers &&
window.webkit.messageHandlers.video
);
}
function isNativeAndroid() {
return (
navigator.userAgent === 'DEV-Native-android' &&
typeof AndroidBridge !== 'undefined' &&
AndroidBridge !== null &&
AndroidBridge.videoMessage !== undefined
);
}
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
function timeToSeconds(hms) {
var a;
if (hms.length < 3) {
return hms;
} else if (hms.length < 6) {
a = hms.split(':');
return (hms = +a[0] * 60 + +a[1]);
} else {
a = hms.split(':');
return (hms = +a[0] * 60 * 60 + +a[1] * 60 + +a[2]);
}
}
function initWebPlayer(seconds, metadata) {
var waitingOnJWP = setInterval(function () {
if (typeof jwplayer !== 'undefined') {
clearInterval(waitingOnJWP);
var playerInstance = jwplayer(`video-player-${metadata.id}`);
playerInstance.setup({
file: metadata.video_source_url,
mediaid: metadata.video_code,
autostart: true,
image: metadata.video_thumbnail_url,
playbackRateControls: true,
tracks: [
{
file: metadata.video_closed_caption_track_url,
label: 'English',
kind: 'captions',
default: false,
},
],
});
if (seconds) {
jwplayer().on('ready', function (event) {
jwplayer().play();
});
jwplayer().on('firstFrame', function () {
jwplayer().seek(seconds);
});
}
}
}, 2);
}
function videoMetadata(videoSource) {
try {
return JSON.parse(videoSource.dataset.meta);
} catch (e) {
console.log('Unable to load Podcast Episode metadata', e); // eslint-disable-line no-console
}
}
function requestFocus() {
var metadata = videoMetadata(videoSource);
var playerElement = getById(`video-player-${metadata.id}`);
getById('pause-butt').classList.add('active');
getById('play-butt').classList.remove('active');
nativeBridgeMessage({
action: 'play',
url: metadata.video_source_url,
seconds: currentTime,
});
}
function handleVideoMessages(mutation) {
if (mutation.type !== 'attributes') {
return;
}
var message = {};
try {
var messageData = getById('video-player-source').dataset.message;
message = JSON.parse(messageData);
} catch (e) {
console.log(e); // eslint-disable-line no-console
return;
}
if (message.action == 'pause') {
getById('pause-butt').classList.remove('active');
getById('play-butt').classList.add('active');
} else if (message.action == 'tick') {
currentTime = message.currentTime;
}
}
function initializePlayer(videoSource) {
var seconds = timeToSeconds(getParameterByName('t') || '0');
var metadata = videoMetadata(videoSource);
if (isNativeIOS()) {
nativeBridgeMessage = function (message) {
try {
window.webkit.messageHandlers.video.postMessage(message);
} catch (err) {
console.log(err.message); // eslint-disable-line no-console
}
};
} else if (isNativeAndroid()) {
nativeBridgeMessage = function (message) {
try {
AndroidBridge.videoMessage(JSON.stringify(message));
} catch (err) {
console.log(err.message); // eslint-disable-line no-console
}
};
} else {
// jwplayer is initialized and no further interaction is needed
initWebPlayer(seconds, metadata);
return;
}
var playerElement = getById(`video-player-${metadata.id}`);
playerElement.addEventListener('click', requestFocus);
playerElement.classList.add('native');
getById('pause-butt').classList.add('active');
var mutationObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
handleVideoMessages(mutation);
});
});
mutationObserver.observe(videoSource, { attributes: true });
currentTime = `${seconds}`;
nativeBridgeMessage({
action: 'play',
url: metadata.video_source_url,
seconds: currentTime,
});
}
// If an video player element is found initialize it
var videoSource = getById('video-player-source');
if (videoSource !== null) {
initializePlayer(videoSource);
}
}

View file

@ -27,6 +27,7 @@
@import 'tag-edit';
@import 'sidebar-data';
@import 'video-collection';
@import 'video-player';
@import 'listings';
@import 'credits';
@import 'item-list';

View file

@ -0,0 +1,40 @@
@import 'variables';
@import '_mixins';
.crayons-article__video {
.crayons-article__video__player {
background-position: center;
background-size: cover;
&.native {
height: calc(100vw * (9/16)); // Maintain 16:9 ratio
}
img {
display: none;
width: calc(100vw * (9/16) * (1/2));
height: calc((100vw * (9/16) * (1/2)) + (100vw * (9/16) * (1/6)));
padding-top: calc(100vw * (9/16) * (1/6));
margin: 0 auto;
&.active {
display: block;
}
}
// All the calculations are kept exactly the same, except they're scaled
// down by (2/3). This because tablets use extra columns and the
// calculations are based on viewport size (the columns "don't count")
@media screen and (min-width: 640px) {
&.native {
height: calc(100vw * (9/16) * (2/3));
}
img {
width: calc(100vw * (9/16) * (1/2) * (2/3));
height: calc((100vw * (9/16) * (1/2) * (2/3)) + (100vw * (9/16) * (1/6) * (2/3)));
padding-top: calc(100vw * (9/16) * (1/6) * (2/3));
}
}
}
}

View file

@ -70,4 +70,14 @@ class ArticleDecorator < ApplicationDecorator
modified_description + " Tagged with #{cached_tag_list}."
end
def video_metadata
{
id: id,
video_code: video_code,
video_source_url: video_source_url,
video_thumbnail_url: cloudinary_video_url,
video_closed_caption_track_url: video_closed_caption_track_url
}
end
end

View file

@ -1,8 +1,11 @@
<div class="crayons-article__video">
<% minutes, seconds = article.video_duration_in_minutes.split(":") %>
<div id="video-player-source" data-source="<%= article.video_source_url %>"></div>
<div id="video-player-source" data-meta="<%= article.decorate.video_metadata.to_json %>" data-message=""></div>
<script src="//content.jwplatform.com/libraries/b1zWy2iv.js" async></script>
<div id="video-player-<%= article.id %>" class="crayons-article__video__player"></div>
<div id="video-player-<%= article.id %>" class="crayons-article__video__player" style="background-image: url('<%= article.cloudinary_video_url %>');">
<img alt="Play Button" id="play-butt" src="<%= image_path("playbutt.png") %>" />
<img alt="Pause Button" id="pause-butt" src="<%= image_path("pausebutt.png") %>" />
</div>
<% unless internal_navigation? || user_signed_in? %>
<script type="application/ld+json">
{
@ -12,63 +15,10 @@
"uploadDate": "<%= article.published_at&.rfc3339 %>",
"name": "<%= article.title %>",
"description": "<%= article.description %>",
"thumbnailUrl": "<%= cloudinary(article.video_thumbnail_url, 880) %>",
"thumbnailUrl": "<%= article.cloudinary_video_url %>",
"contentUrl": "<%= article.video_source_url %>",
"duration": "<%= format("PT%<minutes>sM%<seconds>sS", minutes: minutes, seconds: seconds) %>"
}
</script>
<% end %>
</div>
<script type="text/javascript" async>
function getParameterByName(name, url) {
if (!url) url = window.location.href;
name = name.replace(/[\[\]]/g, "\\$&");
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, " "));
}
function timeToSeconds(hms) {
if (hms.length < 3) {
return hms
} else if (hms.length < 6) {
var a = hms.split(':')
return hms = (+a[0]) * 60 + (+a[1])
} else {
var a = hms.split(':')
return hms = (+a[0]) * 60 * 60 + (+a[1]) * 60 + (+a[2])
}
}
var waitingOnJWP<%= article.id %> = setInterval(function () {
if (typeof jwplayer !== 'undefined') {
clearInterval(waitingOnJWP<%= article.id %>);
var playerInstance = jwplayer("video-player-<%= article.id %>");
playerInstance.setup({
file: "<%= article.video_source_url %>",
mediaid: "<%= article.video_code %>",
autostart: <%= internal_navigation? %>,
image: "<%= cloudinary(article.video_thumbnail_url, 880) %>",
playbackRateControls: true,
tracks: [{
file: "<%= article.video_closed_caption_track_url %>",
label: "English",
kind: "captions",
"default": false
}]
});
var time = getParameterByName('t')
if (time) {
jwplayer().on('ready', function (event) {
jwplayer().play();
});
jwplayer().on('firstFrame', function () {
jwplayer().seek(timeToSeconds(time))
});
}
}
}, 2)
</script>

View file

@ -183,4 +183,24 @@ RSpec.describe ArticleDecorator, type: :decorator do
.description_and_tags).to eq(search_optimized_description_replacement)
end
end
describe "#video_metadata" do
it "responds with a hash representation of video metadata" do
article_with_video = create(:article,
video_code: "ABC",
video_source_url: "https://cdn.com/ABC.m3u8",
video_thumbnail_url: "https://cdn.com/ABC.png",
video_closed_caption_track_url: "https://cdn.com/ABC_captions")
expect(article_with_video.decorate.video_metadata).to eq(
{
id: article_with_video.id,
video_code: article_with_video.video_code,
video_source_url: article_with_video.video_source_url,
video_thumbnail_url: article_with_video.cloudinary_video_url,
video_closed_caption_track_url: article_with_video.video_closed_caption_track_url
},
)
end
end
end