Skip to content

Commit

Permalink
feat(core): add YouTube Shorts-style mobile embed interaction (justin…
Browse files Browse the repository at this point in the history
…ribeiro#67)

* feat(core): add YouTube Shorts-style mobile embed interaction
* fix: make the loop work with the playlist hack
  • Loading branch information
justinribeiro committed Jul 12, 2022
1 parent 0f0bd68 commit ce2a16e
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 16 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[![npm version](https://badge.fury.io/js/@justinribeiro%2Flite-youtube.svg)](https://badge.fury.io/js/@justinribeiro%2Flite-youtube) ![min+gzip](https://img.shields.io/badge/min%2Bgzip-2.2kb-blue) ![min+br](https://img.shields.io/badge/min%2Bbr-1.7kb-blue) [![](https://data.jsdelivr.com/v1/package/npm/@justinribeiro/lite-youtube/badge)](https://www.jsdelivr.com/package/npm/@justinribeiro/lite-youtube)

![Statements](https://img.shields.io/badge/statements-97.7%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-86.66%25-yellow.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-88.46%25-yellow.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-97.7%25-brightgreen.svg?style=flat)
![Statements](https://img.shields.io/badge/statements-98.22%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-91.17%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-100%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-98.22%25-brightgreen.svg?style=flat)

# \<lite-youtube\>

Expand Down Expand Up @@ -130,6 +130,7 @@ flexibility.
| `posterloading`| Set img lazy load attr `loading` for poster image | `lazy` |
| `nocookie` | Use youtube-nocookie.com as iframe embed uri | `false` |
| `autoload` | Use Intersection Observer to load iframe when scrolled into view | `false` |
| `short` | Show 9:16 YouTube Shorts-style interaction on mobile devices | `false` |
| `params` | Set YouTube query parameters | `` |

## Events
Expand Down
1 change: 1 addition & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,6 @@ <h3>YouTube Playlist</h3>
videoid="VLrYOji75Vc"
playlistid="PL-G5r6j4GptH5JTveoLTVqpp7w2oc27Q9"
></lite-youtube>

</body>
</html>
57 changes: 57 additions & 0 deletions demo/shorts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes"
/>
<title>lite-youtube demo</title>
<script type="module" src="../lite-youtube.js"></script>
<style>
* {
box-sizing: border-box;
}
div {

padding: 1rem;
}
body {
max-width: 800px;
margin: auto;
}
pre {
width: 100%;
padding: 1em;
overflow-x: scroll;
background-clip: z;
background-color: #eee;
}
.styleIt {
width: 400px;
}
#bigBlock {
height: 600px;
}
</style>
</head>
<body>
<div>
<h3>YouTube Short testing</h3>
<pre>

&lt;lite-youtube videoid=&quot;vMImN9gghao&quot; short&gt;&lt;/lite-youtube&gt;
&lt;lite-youtube videoid=&quot;k3ECbBj4UZ8&quot; short&gt;&lt;/lite-youtube&gt;
&lt;lite-youtube videoid=&quot;J0xuH5uEVv4&quot; short&gt;&lt;/lite-youtube&gt;
&lt;lite-youtube videoid=&quot;oGVKPWz3RJY&quot; short&gt;&lt;/lite-youtube&gt;
&lt;lite-youtube videoid=&quot;bqiOiTTtUZ4&quot; short&gt;&lt;/lite-youtube&gt;

</pre>
</div>
<lite-youtube videoid="vMImN9gghao" short></lite-youtube>
<lite-youtube videoid="k3ECbBj4UZ8" short></lite-youtube>
<lite-youtube videoid="J0xuH5uEVv4" short></lite-youtube>
<lite-youtube videoid="oGVKPWz3RJY" short></lite-youtube>
<lite-youtube videoid="bqiOiTTtUZ4" short></lite-youtube>
</body>
</html>
76 changes: 63 additions & 13 deletions lite-youtube.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,8 @@ export class LiteYTEmbed extends HTMLElement {
this.setAttribute('videoPlay', name);
}

get videoStartAt(): number {
return Number(this.getAttribute('videoStartAt') || '0');
}

set videoStartAt(time: number) {
this.setAttribute('videoStartAt', String(time));
get videoStartAt(): string {
return this.getAttribute('videoStartAt') || '0';
}

get autoLoad(): boolean {
Expand All @@ -106,6 +102,10 @@ export class LiteYTEmbed extends HTMLElement {
return `start=${this.videoStartAt}&${this.getAttribute('params')}`;
}

set params(opts: string) {
this.setAttribute('params', opts);
}

/**
* Define our shadowDOM for the component
*/
Expand All @@ -124,6 +124,12 @@ export class LiteYTEmbed extends HTMLElement {
--lyt-play-btn-hover: #f00;
}
@media (max-width: 40em) {
:host([short]) {
padding-bottom: calc(100% / (9 / 16));
}
}
#frame, #fallbackPlaceholder, iframe {
position: absolute;
width: 100%;
Expand Down Expand Up @@ -223,7 +229,7 @@ export class LiteYTEmbed extends HTMLElement {
);
this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`);

if (this.autoLoad) {
if (this.autoLoad || this.isYouTubeShort()) {
this.initIntersectionObserver();
}
}
Expand All @@ -241,7 +247,9 @@ export class LiteYTEmbed extends HTMLElement {
): void {
switch (name) {
case 'videoid':
case 'playlistid': {
case 'playlistid':
case 'videoTitle':
case 'videoPlay': {
if (oldVal !== newVal) {
this.setupComponent();

Expand All @@ -266,14 +274,21 @@ export class LiteYTEmbed extends HTMLElement {
private addIframe(isIntersectionObserver = false): void {
if (!this.isIframeLoaded) {
// Don't autoplay the intersection observer injection, it's weird
const autoplay = isIntersectionObserver ? 0 : 1;
let autoplay = isIntersectionObserver ? 0 : 1;
const wantsNoCookie = this.noCookie ? '-nocookie' : '';
let embedTarget;
if (this.playlistId) {
embedTarget = `?listType=playlist&list=${this.playlistId}&`;
} else {
embedTarget = `${this.videoId}?`;
}

// Oh wait, you're a YouTube short, so let's try to make you more workable
if (this.isYouTubeShort()) {
this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`;
autoplay = 1;
}

const iframeHTML = `
<iframe frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen
Expand All @@ -282,6 +297,7 @@ export class LiteYTEmbed extends HTMLElement {
this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML);
this.domRefFrame.classList.add('activated');
this.isIframeLoaded = true;
this.attemptShortAutoPlay();
this.dispatchEvent(
new CustomEvent('liteYoutubeIframeLoaded', {
detail: {
Expand Down Expand Up @@ -340,19 +356,53 @@ export class LiteYTEmbed extends HTMLElement {
observer.observe(this);
}

/**
* This is a terrible hack to attempt to get YouTube Short-like autoplay on
* mobile viewports. It's this way because:
* 1. YouTube's Iframe embed does not offer determinism when loading
* 2. Attempting to use onYouTubeIframeAPIReady() does not work in 99% of
* cases
* 3. You can _technically_ load the Frame API library and do more advanced
* things, but I don't want to burn the thread of the wire with its
* shenanigans since this an edge case.
* @private
*/
private attemptShortAutoPlay() {
if (this.isYouTubeShort()) {
setTimeout(() => {
this.shadowRoot
.querySelector('iframe')
?.contentWindow?.postMessage(
'{"event":"command","func":"' + 'playVideo' + '","args":""}',
'*'
);
// for youtube video recording demo
}, 2000);
}
}

/**
* A hacky attr check and viewport peek to see if we're going to try to enable
* a more friendly YouTube Short style loading
* @returns boolean
*/
private isYouTubeShort(): boolean {
return (
this.getAttribute('short') === '' &&
window.matchMedia('(max-width: 40em)').matches
);
}

/**
* Add a <link rel={preload | preconnect} ...> to the head
* @param {string} kind
* @param {string} url
* @param {string} as
*/
private static addPrefetch(kind: string, url: string, as?: string): void {
private static addPrefetch(kind: string, url: string): void {
const linkElem = document.createElement('link');
linkElem.rel = kind;
linkElem.href = url;
if (as) {
linkElem.as = as;
}
linkElem.crossOrigin = 'true';
document.head.append(linkElem);
}
Expand Down
40 changes: 38 additions & 2 deletions test/lite-youtube.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { setViewport } from '@web/test-runner-commands';
import { LiteYTEmbed } from '../lite-youtube.js';
import '../lite-youtube.js';

const baseTemplate = html`<lite-youtube videoid="guJLfqTFfIw"></lite-youtube>`;
const baseTemplate = html`<lite-youtube videoTitle="Test Me" videoid="guJLfqTFfIw"></lite-youtube>`;

describe('<lite-youtube>', () => {
it('attr sets the videoid', async () => {
Expand All @@ -19,6 +19,20 @@ describe('<lite-youtube>', () => {
expect(el.videoId).to.equal('tests');
});

it('videoTitle set/get', async () => {
const el = await fixture<LiteYTEmbed>(baseTemplate);
expect(el.videoTitle).to.equal('Test Me');
el.videoTitle = 'Test Me Again';
expect(el.videoTitle).to.equal('Test Me Again');
});

it('videoPlay set/get', async () => {
const el = await fixture<LiteYTEmbed>(baseTemplate);
expect(el.videoPlay).to.equal('Play');
el.videoPlay = 'Run';
expect(el.videoPlay).to.equal('Run');
});

it('clicking button should load iframe', async () => {
const el = await fixture<LiteYTEmbed>(baseTemplate);
expect(el.shadowRoot.querySelector('iframe')).to.be.null;
Expand Down Expand Up @@ -64,7 +78,7 @@ describe('<lite-youtube>', () => {
);
// this is a cheeky test by counting the test runner + the warm injector
// TODO write a better observer
expect(document.head.querySelectorAll('link').length).to.be.equal(10);
expect(document.head.querySelectorAll('link').length).to.be.equal(12);
});

it('nocookie attr should change iframe url target', async () => {
Expand Down Expand Up @@ -130,6 +144,28 @@ describe('<lite-youtube>', () => {
expect(fallback?.loading).to.be.equal('eager');
});

it('YouTube Short desktop check', async () => {
const el = await fixture<LiteYTEmbed>(
html`<lite-youtube
videoid="guJLfqTFfIw"
short
></lite-youtube>`
);
expect(el['isYouTubeShort']()).to.be.equal(false);
});

it('YouTube Short mobile check', async () => {
setViewport({ width: 360, height: 640 });
const el = await fixture<LiteYTEmbed>(
html`<lite-youtube
videoid="guJLfqTFfIw"
short
></lite-youtube>`
);
el.click();
expect(el['isYouTubeShort']()).to.be.equal(true);
});

it('is valid A11y via aXe', async () => {
const el = await fixture<LiteYTEmbed>(baseTemplate);
await expect(el).shadowDom.to.be.accessible();
Expand Down

0 comments on commit ce2a16e

Please sign in to comment.