Skip to content

Commit

Permalink
✨ Youtube 1.0: get duration, currentTime (#32804)
Browse files Browse the repository at this point in the history
For #30443
  • Loading branch information
alanorozco committed Feb 23, 2021
1 parent 1e71bbd commit 319372d
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 22 deletions.
81 changes: 62 additions & 19 deletions extensions/amp-youtube/1.0/component.js
Expand Up @@ -21,7 +21,9 @@ import {VideoWrapper} from '../../amp-video/1.0/video-wrapper';
import {addParamsToUrl} from '../../../src/url';
import {dict} from '../../../src/utils/object';
import {dispatchCustomEvent} from '../../../src/dom';
import {forwardRef} from '../../../src/preact/compat';
import {mutedOrUnmutedEvent, objOrParseJson} from '../../../src/iframe-video';
import {useImperativeHandle, useState} from '../../../src/preact';

// Correct PlayerStates taken from
// https://developers.google.com/youtube/iframe_api_reference#Playback_status
Expand Down Expand Up @@ -60,19 +62,28 @@ const PlayerFlags = {
/** @const {!../../../src/dom.CustomEventOptionsDef} */
const VIDEO_EVENT_OPTIONS = {bubbles: false, cancelable: false};

/**
* Created once per component mount.
* The fields returned can be overridden by `infoDelivery` messages.
* @return {!JsonObject}
*/
function createDefaultInfo() {
return dict({
'currentTime': 0,
'duration': NaN,
});
}

/**
* @param {!YoutubeProps} props
* @param {{current: (T|null)}} ref
* @return {PreactDef.Renderable}
* @template T
*/
export function Youtube({
autoplay,
loop,
videoid,
liveChannelid,
params = {},
credentials,
...rest
}) {
function YoutubeWithRef(
{autoplay, loop, videoid, liveChannelid, params = {}, credentials, ...rest},
ref
) {
const datasourceExists =
!(videoid && liveChannelid) && (videoid || liveChannelid);

Expand Down Expand Up @@ -115,21 +126,46 @@ export function Youtube({

src = addParamsToUrl(src, params);

const [info] = useState(createDefaultInfo);

useImperativeHandle(
ref,
() => ({
get currentTime() {
return info['currentTime'];
},
get duration() {
return info['duration'];
},
}),
[info]
);

const onMessage = ({data, currentTarget}) => {
data = objOrParseJson(data);
if (data == null) {
const parsedData = objOrParseJson(data);
if (!parsedData) {
return;
}
if (data.event == 'initialDelivery') {

const {'event': event, 'info': parsedInfo} = parsedData;

if (event == 'initialDelivery') {
dispatchVideoEvent(currentTarget, VideoEvents.LOADEDMETADATA);
return;
}
const {info} = data;
if (info == undefined) {

if (!parsedInfo) {
return;
}
const playerState = info['playerState'];
if (data.event == 'infoDelivery' && playerState == 0 && loop) {

for (const key in info) {
if (parsedInfo[key] != null) {
info[key] = parsedInfo[key];
}
}

const playerState = parsedInfo['playerState'];
if (event == 'infoDelivery' && playerState == 0 && loop) {
currentTarget.contentWindow./*OK*/ postMessage(
JSON.stringify(
dict({
Expand All @@ -140,11 +176,14 @@ export function Youtube({
'*'
);
}
if (data.event == 'infoDelivery' && playerState != undefined) {
if (event == 'infoDelivery' && playerState != undefined) {
dispatchVideoEvent(currentTarget, PlayerStates[playerState.toString()]);
}
if (data.event == 'infoDelivery' && info['muted']) {
dispatchVideoEvent(currentTarget, mutedOrUnmutedEvent(info['muted']));
if (event == 'infoDelivery' && parsedInfo['muted']) {
dispatchVideoEvent(
currentTarget,
mutedOrUnmutedEvent(parsedInfo['muted'])
);
return;
}
};
Expand Down Expand Up @@ -218,3 +257,7 @@ function makeMethodMessage(method) {
})
);
}

const Youtube = forwardRef(YoutubeWithRef);
Youtube.displayName = 'Youtube'; // Make findable for tests.
export {Youtube};
75 changes: 72 additions & 3 deletions extensions/amp-youtube/1.0/storybook/Basic.js
Expand Up @@ -23,6 +23,7 @@ import {
} from '../../../amp-accordion/1.0/component';
import {Youtube} from '../component';
import {boolean, number, object, text, withKnobs} from '@storybook/addon-knobs';
import {useRef, useState} from '../../../../src/preact';
import {withA11y} from '@storybook/addon-a11y';

export default {
Expand All @@ -31,10 +32,13 @@ export default {
decorators: [withA11y, withKnobs],
};

const VIDEOID = 'IAvf-rkzNck';
const LIVE_CHANNEL_ID = 'sKCkM-f2Qk4';

export const _default = () => {
const width = number('width', 300);
const height = number('height', 200);
const videoid = text('videoid', 'IAvf-rkzNck');
const videoid = text('videoid', VIDEOID);
const autoplay = boolean('autoplay', false);
const loop = boolean('loop', false);
const params = object('params', {});
Expand All @@ -51,10 +55,75 @@ export const _default = () => {
);
};

/**
* @param {*} props
* @return {*}
*/
function WithStateTable({videoid, autoplay, loop, params, credentials, style}) {
const ref = useRef(null);

const [stateTable, setStateTable] = useState(null);
const setCurrentStateTable = () => {
setStateTable(
<table>
{['autoplay', 'controls', 'loop', 'currentTime', 'duration'].map(
(key) => (
<tr key={key}>
<td>{key}</td>
<td>{ref.current[key]}</td>
</tr>
)
)}
</table>
);
};

return (
<>
<Youtube
ref={ref}
autoplay={autoplay}
loop={loop}
videoid={videoid}
params={params}
style={style}
credentials={credentials}
/>
<p>
<button onClick={setCurrentStateTable}>🔄 current state</button>
</p>
{stateTable}
</>
);
}

/**
* @return {*}
*/
export function State() {
const width = number('width', 300);
const height = number('height', 200);
const videoid = text('videoid', VIDEOID);
const autoplay = boolean('autoplay', false);
const loop = boolean('loop', false);
const params = object('params', {});
const credentials = text('credentials', 'include');
return (
<WithStateTable
autoplay={autoplay}
loop={loop}
videoid={videoid}
params={params}
style={{width, height}}
credentials={credentials}
/>
);
}

export const liveChannelId = () => {
const width = number('width', 300);
const height = number('height', 200);
const liveChannelid = text('liveChannelid', 'sKCkM-f2Qk4');
const liveChannelid = text('liveChannelid', LIVE_CHANNEL_ID);
const autoplay = boolean('autoplay', false);
const loop = boolean('loop', false);
const params = object('params', {});
Expand All @@ -74,7 +143,7 @@ export const liveChannelId = () => {
export const InsideAccordion = () => {
const width = text('width', '320px');
const height = text('height', '180px');
const videoid = text('videoid', 'IAvf-rkzNck');
const videoid = text('videoid', VIDEOID);
const params = object('params', {});
return (
<Accordion expandSingleSection>
Expand Down
39 changes: 39 additions & 0 deletions extensions/amp-youtube/1.0/test/test-youtube.js
Expand Up @@ -16,6 +16,8 @@

import * as Preact from '../../../../src/preact';
import {Youtube} from '../component';
import {createRef} from '../../../../src/preact';

import {dispatchCustomEvent} from '../../../../src/dom';
import {mount} from 'enzyme';

Expand Down Expand Up @@ -155,4 +157,41 @@ describes.realWin('YouTube preact component v1.0', {}, (env) => {
event: 'listening',
});
});

it('exposes properties from info messages', () => {
const ref = createRef();

const wrapper = mount(
<Youtube
ref={ref}
videoid="IAvf-rkzNck"
shortcode="B8QaZW4AQY_"
style={{width: 500, height: 600}}
/>,
{attachTo: document.body}
);

const {contentWindow} = wrapper.getDOMNode().querySelector('iframe');

expect(ref.current.currentTime).to.equal(0);
expect(ref.current.duration).to.be.NaN;

mockMessage(window, contentWindow, {info: {duration: 420}});

expect(ref.current.currentTime).to.equal(0);
expect(ref.current.duration).to.equal(420);

mockMessage(window, contentWindow, {info: {currentTime: 12.3}});

expect(ref.current.currentTime).to.equal(12.3);
expect(ref.current.duration).to.equal(420);
});
});

function mockMessage(win, source, data) {
const mockEvent = new CustomEvent('message');
mockEvent.data = JSON.stringify(data);
mockEvent.source = source;
win.dispatchEvent(mockEvent);
return mockEvent;
}

0 comments on commit 319372d

Please sign in to comment.