Add Native bridge video support (#9303)
This commit is contained in:
parent
f1b3b5a501
commit
154a3629ee
7 changed files with 275 additions and 57 deletions
|
|
@ -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();
|
||||
|
|
|
|||
195
app/assets/javascripts/initializers/initializeVideoPlayback.js
Normal file
195
app/assets/javascripts/initializers/initializeVideoPlayback.js
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
@import 'tag-edit';
|
||||
@import 'sidebar-data';
|
||||
@import 'video-collection';
|
||||
@import 'video-player';
|
||||
@import 'listings';
|
||||
@import 'credits';
|
||||
@import 'item-list';
|
||||
|
|
|
|||
40
app/assets/stylesheets/video-player.scss
Normal file
40
app/assets/stylesheets/video-player.scss
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue