зеркало из https://github.com/GoogleChrome/kino.git
Коммит
cd26b29d6c
|
@ -4,7 +4,8 @@
|
|||
"https://kinoweb-dev.web.app",
|
||||
"https://kinoweb-dev--staging-2ra3ji0i.web.app",
|
||||
"https://kinoweb.dev",
|
||||
"http://localhost:5000"
|
||||
"http://localhost:5000",
|
||||
"https://www.gstatic.com"
|
||||
],
|
||||
"responseHeader": [
|
||||
"Accept-Ranges",
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"headers": [
|
||||
{
|
||||
"key": "Content-Security-Policy",
|
||||
"value": "script-src 'self'; object-src 'none'; base-uri 'none'"
|
||||
"value": "script-src 'self' www.gstatic.com; object-src 'none'; base-uri 'none'"
|
||||
}
|
||||
]
|
||||
}],
|
||||
|
|
|
@ -8,6 +8,7 @@ length: '1:04'
|
|||
video-sources:
|
||||
- src: https://storage.googleapis.com/kino-assets/single-video/video.mp4
|
||||
type: video/mp4; codecs="avc1.640032,mp4a.40.2"
|
||||
cast: true
|
||||
thumbnail: https://storage.googleapis.com/kino-assets/single-video/thumbnail.png
|
||||
media-session-artwork:
|
||||
- sizes: 96x96
|
||||
|
|
|
@ -10,6 +10,7 @@ video-sources:
|
|||
type: video/mp4; codecs="av01.0.04M.08, mp4a.40.2"
|
||||
- src: https://storage.googleapis.com/kino-assets/multiple-sources/hevc.mp4
|
||||
type: video/mp4; codecs="hev1.1.6.L93.90,mp4a.40.2"
|
||||
cast: true
|
||||
- src: https://storage.googleapis.com/kino-assets/multiple-sources/vp9.webm
|
||||
type: video/webm
|
||||
thumbnail: https://storage.googleapis.com/kino-assets/multiple-sources/thumbnail.png
|
||||
|
|
|
@ -10,6 +10,7 @@ video-sources:
|
|||
type: video/mp4; codecs="av01.0.04M.08, mp4a.40.2"
|
||||
- src: https://storage.googleapis.com/kino-assets/using-webvtt/hevc.mp4
|
||||
type: video/mp4; codecs="hev1.1.6.L93.90,mp4a.40.2"
|
||||
cast: true
|
||||
- src: https://storage.googleapis.com/kino-assets/using-webvtt/vp9.webm
|
||||
type: video/webm
|
||||
video-subtitles:
|
||||
|
@ -21,8 +22,8 @@ video-subtitles:
|
|||
- default: false
|
||||
kind: captions
|
||||
label: Česky
|
||||
src: https://storage.googleapis.com/kino-assets/using-webvtt/cap-cz.vtt
|
||||
srclang: cz
|
||||
src: https://storage.googleapis.com/kino-assets/using-webvtt/cap-cs.vtt
|
||||
srclang: cs
|
||||
thumbnail: https://storage.googleapis.com/kino-assets/using-webvtt/thumbnail.png
|
||||
media-session-artwork:
|
||||
- sizes: 96x96
|
||||
|
|
|
@ -8,6 +8,7 @@ length: '1:04'
|
|||
video-sources:
|
||||
- src: https://storage.googleapis.com/kino-assets/streaming-basics/manifest.mpd
|
||||
type: application/dash+xml
|
||||
cast: true
|
||||
- src: https://storage.googleapis.com/kino-assets/streaming-basics/master.m3u8
|
||||
type: application/x-mpegURL
|
||||
url-rewrites:
|
||||
|
@ -22,8 +23,8 @@ video-subtitles:
|
|||
- default: false
|
||||
kind: captions
|
||||
label: Česky
|
||||
src: https://storage.googleapis.com/kino-assets/streaming-basics/cap-cz.vtt
|
||||
srclang: cz
|
||||
src: https://storage.googleapis.com/kino-assets/streaming-basics/cap-cs.vtt
|
||||
srclang: cs
|
||||
thumbnail: https://storage.googleapis.com/kino-assets/streaming-basics/thumbnail.png
|
||||
media-session-artwork:
|
||||
- sizes: 96x96
|
||||
|
|
|
@ -8,6 +8,7 @@ length: '1:04'
|
|||
video-sources:
|
||||
- src: https://storage.googleapis.com/kino-assets/efficient-formats/manifest.mpd
|
||||
type: application/dash+xml
|
||||
cast: true
|
||||
- src: https://storage.googleapis.com/kino-assets/efficient-formats/master.m3u8
|
||||
type: application/x-mpegURL
|
||||
url-rewrites:
|
||||
|
@ -22,8 +23,8 @@ video-subtitles:
|
|||
- default: false
|
||||
kind: captions
|
||||
label: Česky
|
||||
src: https://storage.googleapis.com/kino-assets/efficient-formats/cap-cz.vtt
|
||||
srclang: cz
|
||||
src: https://storage.googleapis.com/kino-assets/efficient-formats/cap-cs.vtt
|
||||
srclang: cs
|
||||
thumbnail: https://storage.googleapis.com/kino-assets/efficient-formats/thumbnail.png
|
||||
media-session-artwork:
|
||||
- sizes: 96x96
|
||||
|
|
|
@ -8,6 +8,7 @@ length: '1:04'
|
|||
video-sources:
|
||||
- src: https://storage.googleapis.com/kino-assets/adaptive-streaming/manifest.mpd
|
||||
type: application/dash+xml
|
||||
cast: true
|
||||
- src: https://storage.googleapis.com/kino-assets/adaptive-streaming/master.m3u8
|
||||
type: application/x-mpegURL
|
||||
url-rewrites:
|
||||
|
@ -22,8 +23,8 @@ video-subtitles:
|
|||
- default: false
|
||||
kind: captions
|
||||
label: Česky
|
||||
src: https://storage.googleapis.com/kino-assets/adaptive-streaming/cap-cz.vtt
|
||||
srclang: cz
|
||||
src: https://storage.googleapis.com/kino-assets/adaptive-streaming/cap-cs.vtt
|
||||
srclang: cs
|
||||
thumbnail: https://storage.googleapis.com/kino-assets/adaptive-streaming/thumbnail.png
|
||||
media-session-artwork:
|
||||
- sizes: 96x96
|
||||
|
|
47
src/index.js
47
src/index.js
|
@ -152,3 +152,50 @@ if ('serviceWorker' in navigator) {
|
|||
navigator.serviceWorker.register('/sw.js');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a global closure that ensures that Google Cast
|
||||
* is only initialized once and allows the cast button
|
||||
* to be reused across pages.
|
||||
*/
|
||||
window.kinoInitGoogleCast = (function kinoInitGoogleCastIIFE() {
|
||||
let castButtonPromise = false;
|
||||
|
||||
/**
|
||||
* Initializes Google Cast Web Sender and returns a promise resolving
|
||||
* with a cast button.
|
||||
*
|
||||
* @returns {Promise<HTMLElement>} Custom <google-cast-launcher> element.
|
||||
*/
|
||||
return () => {
|
||||
if (!castButtonPromise) {
|
||||
castButtonPromise = new Promise((resolve) => {
|
||||
const initCastApi = () => {
|
||||
window.cast.framework.CastContext.getInstance().setOptions({
|
||||
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
||||
});
|
||||
|
||||
const castButton = document.createElement('button');
|
||||
const castCustomElement = document.createElement('google-cast-launcher');
|
||||
|
||||
castButton.setAttribute('aria-label', 'Cast this video');
|
||||
castButton.appendChild(castCustomElement);
|
||||
|
||||
resolve(castButton);
|
||||
};
|
||||
|
||||
window.__onGCastApiAvailable = (isAvailable) => {
|
||||
if (isAvailable) {
|
||||
initCastApi();
|
||||
}
|
||||
};
|
||||
|
||||
const scriptEl = document.createElement('script');
|
||||
scriptEl.type = 'text/javascript';
|
||||
scriptEl.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
||||
document.head.appendChild(scriptEl);
|
||||
});
|
||||
}
|
||||
return castButtonPromise;
|
||||
};
|
||||
}());
|
||||
|
|
|
@ -130,3 +130,10 @@ export const IDB_DATA_CHANGED_EVENT = 'idb-data-changed';
|
|||
* Picture in picture.
|
||||
*/
|
||||
export const PIP_CLASSNAME = 'picture-in-picture';
|
||||
|
||||
/**
|
||||
* Casting.
|
||||
*/
|
||||
export const CAST_CLASSNAME = 'cast';
|
||||
export const CAST_HAS_TARGET_NAME = 'cast-has-target';
|
||||
export const CAST_TARGET_NAME = 'cast-target-name';
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 2;
|
||||
z-index: auto; /* In order to not interfere with the mobile slide out menu. */
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
|
@ -27,7 +27,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
:host .floating-buttons button {
|
||||
:host .floating-buttons > * {
|
||||
border-radius: 8px;
|
||||
background-color: var(--accent-background);
|
||||
width: 48px;
|
||||
|
@ -38,12 +38,19 @@
|
|||
border: none;
|
||||
}
|
||||
|
||||
button google-cast-launcher {
|
||||
height: 24px;
|
||||
width: auto;
|
||||
--connected-color: var(--accent);
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.pip-overlay {
|
||||
.pip-overlay,
|
||||
.cast-overlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
@ -53,20 +60,33 @@ video {
|
|||
font-size: clamp(12px, 4vw, 24px);
|
||||
}
|
||||
|
||||
.pip-overlay svg {
|
||||
.pip-overlay svg,
|
||||
.cast-overlay svg {
|
||||
align-self: end;
|
||||
width: clamp(40px, 20vw, 128px);
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:host(.picture-in-picture) .pip-overlay {
|
||||
:host(.picture-in-picture) .pip-overlay,
|
||||
:host(.cast) .cast-overlay {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.cast-overlay .cast-target {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host(.cast-has-target) .cast-overlay .cast-target {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media (min-width: 720px) {
|
||||
:host(.picture-in-picture) .pip-overlay {
|
||||
row-gap: 32px;
|
||||
}
|
||||
:host(.cast) .cast-overlay {
|
||||
row-gap: 32px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,9 @@ import ParserMPD from '../../classes/ParserMPD';
|
|||
import selectSource from '../../utils/selectSource';
|
||||
|
||||
import {
|
||||
CAST_CLASSNAME,
|
||||
CAST_HAS_TARGET_NAME,
|
||||
CAST_TARGET_NAME,
|
||||
MEDIA_SESSION_DEFAULT_ARTWORK,
|
||||
PIP_CLASSNAME,
|
||||
} from '../../constants';
|
||||
|
@ -109,7 +112,11 @@ export default class extends HTMLElement {
|
|||
<div class="floating-buttons"></div>
|
||||
<div class="pip-overlay">
|
||||
<svg viewBox="0 0 129 128" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M108.5 48V16a8.001 8.001 0 0 0-8-8h-84a8 8 0 0 0-8 8v68a8 8 0 0 0 8 8h20" stroke="var(--icon)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M52.5 112V72a8 8 0 0 1 8-8h52a8 8 0 0 1 8 8v40a8 8 0 0 1-8 8h-52a8 8 0 0 1-8-8Z" stroke="var(--icon)" stroke-width="3" stroke-miterlimit="10" stroke-linecap="square"/></svg>
|
||||
This video is playing in picture in picture
|
||||
<p>This video is playing in picture in picture</p>
|
||||
</div>
|
||||
<div class="cast-overlay">
|
||||
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" stroke-linecap="round" stroke-miterlimit="10"><path d="M34.133 107.201c0-13.253-10.747-24-24-24M53.333 107.2c0-23.861-19.339-43.2-43.2-43.2" fill="none" stroke="var(--icon)" stroke-width="8"/><path d="M10.133 112.001a4.8 4.8 0 1 0 0-9.6 4.8 4.8 0 0 0 0 9.6Z" fill="var(--icon)" fill-rule="nonzero"/><path d="M5.333 49.778V32c0-5.891 4.776-10.667 10.667-10.667h96c5.891 0 10.667 4.776 10.667 10.667v64c0 5.891-4.776 10.667-10.667 10.667H72.381" fill="none" stroke="var(--icon)" stroke-width="8"/></svg>
|
||||
<p>Casting<span class="cast-target"> to <span class="cast-target-name"></span></span></p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -131,6 +138,85 @@ export default class extends HTMLElement {
|
|||
floatingButtonsBar.appendChild(pipButton);
|
||||
}
|
||||
|
||||
window.kinoInitGoogleCast().then((castButton) => {
|
||||
floatingButtonsBar.appendChild(castButton);
|
||||
|
||||
window.cast.framework.CastContext.getInstance().addEventListener(
|
||||
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
||||
async (e) => {
|
||||
if (e.sessionState === 'SESSION_STARTED' || e.sessionState === 'SESSION_RESUMED') {
|
||||
const castableSources = this.internal.videoData['video-sources'].filter((source) => source.cast === true);
|
||||
|
||||
if (!castableSources) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error('[Google Cast] The media has no source suitable for casting.');
|
||||
return;
|
||||
}
|
||||
|
||||
const castSession = window.cast.framework.CastContext.getInstance().getCurrentSession();
|
||||
const mediaInfo = new window.chrome.cast.media.MediaInfo(
|
||||
castableSources[0].src,
|
||||
castableSources[0].type,
|
||||
);
|
||||
const videoThumbnail = new window.chrome.cast.Image(this.internal.videoData.thumbnail);
|
||||
const metadata = new window.chrome.cast.media.GenericMediaMetadata();
|
||||
|
||||
metadata.title = videoData.title;
|
||||
|
||||
/**
|
||||
* @todo Add the Media Session artwork and define image dimensions explicitly.
|
||||
*/
|
||||
metadata.images = [videoThumbnail];
|
||||
mediaInfo.metadata = metadata;
|
||||
|
||||
/** @type {Array} */
|
||||
const subtitles = this.internal.videoData['video-subtitles'] || [];
|
||||
const defaultSubtitles = subtitles.find((subtitle) => subtitle.default);
|
||||
|
||||
/**
|
||||
* AFAICT the Default Media Receiver doesn't implement any UI to
|
||||
* select the subtitle track.
|
||||
*
|
||||
* We only add the subtitle track if there is a default one.
|
||||
*/
|
||||
if (defaultSubtitles) {
|
||||
const defaultSubtitlesTrack = new window.chrome.cast.media.Track(
|
||||
1,
|
||||
window.chrome.cast.media.TrackType.TEXT,
|
||||
);
|
||||
|
||||
defaultSubtitlesTrack.trackContentId = defaultSubtitles.src;
|
||||
defaultSubtitlesTrack.subtype = window.chrome.cast.media.TextTrackType.SUBTITLES;
|
||||
defaultSubtitlesTrack.name = defaultSubtitles.label;
|
||||
defaultSubtitlesTrack.language = defaultSubtitles.srclang;
|
||||
defaultSubtitlesTrack.trackContentType = 'text/vtt';
|
||||
|
||||
mediaInfo.tracks = [defaultSubtitlesTrack];
|
||||
}
|
||||
|
||||
const request = new window.chrome.cast.media.LoadRequest(mediaInfo);
|
||||
|
||||
try {
|
||||
await castSession.loadMedia(request);
|
||||
} catch (error) {
|
||||
/* eslint-disable-next-line no-console */
|
||||
console.error(`[Google Cast] Error code: ${error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetName = castSession.getCastDevice().friendlyName;
|
||||
this.internal.root.querySelector(`.${CAST_TARGET_NAME}`).innerText = targetName;
|
||||
this.classList.toggle(CAST_HAS_TARGET_NAME, targetName);
|
||||
this.classList.add(CAST_CLASSNAME);
|
||||
}
|
||||
|
||||
if (e.sessionState === 'SESSION_ENDED') {
|
||||
this.classList.remove(CAST_CLASSNAME);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up Media Session API integration.
|
||||
*/
|
||||
|
|
Загрузка…
Ссылка в новой задаче