diff --git a/examples/browser/web/index.html b/examples/browser/web/index.html index b69d6b3b..d35acdeb 100644 --- a/examples/browser/web/index.html +++ b/examples/browser/web/index.html @@ -1,21 +1,23 @@ - + + YouTube.js Example -
-
-
- +
+
+
+ +


@@ -26,5 +28,4 @@ - \ No newline at end of file diff --git a/examples/browser/web/package.json b/examples/browser/web/package.json index 284684c0..54593919 100644 --- a/examples/browser/web/package.json +++ b/examples/browser/web/package.json @@ -13,6 +13,6 @@ "vite": "^3.0.0" }, "dependencies": { - "dashjs": "^4.4.0" + "shaka-player": "^4.3.8" } } \ No newline at end of file diff --git a/examples/browser/web/src/assets/player.css b/examples/browser/web/src/assets/player.css new file mode 100644 index 00000000..ce4a420d --- /dev/null +++ b/examples/browser/web/src/assets/player.css @@ -0,0 +1,423 @@ +@import url(https://fonts.googleapis.com/css?family=Material+Icons+Sharp); + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v27/KFOmCnqEu92Fr1Me5Q.ttf) format('truetype'); +} + +@font-face { + font-family: 'Roboto'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(https://fonts.gstatic.com/s/roboto/v27/KFOlCnqEu92Fr1MmEU9vAw.ttf) format('truetype'); +} + +.shaka-container { + font-family: 'Roboto', sans-serif; +} + +.shaka-container .shaka-bottom-controls { + width: 100%; + padding: 0; + padding-bottom: 0; + z-index: 1; +} + +.shaka-container .shaka-bottom-controls { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + -ms-flex-direction: column; + flex-direction: column; +} + +.shaka-container .shaka-ad-controls { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; +} + +.shaka-container .shaka-spinner .shaka-spinner-path { + stroke: #ffffff; +} + +.shaka-container .shaka-scrim-container { + margin: 0; + width: 100%; + height: 100%; + flex-shrink: 1; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + opacity: 0; + transition: opacity cubic-bezier(.4, 0, .6, 1) .6s; + background: linear-gradient(to top, hsla(0, 0%, 0%, 0.61), transparent 15%); +} + +.shaka-container .shaka-play-button { + width: 100px; + height: 100px; + border-radius: 0; + background-color: transparent; + filter: invert(); + box-shadow: none; + -webkit-box-ordinal-group: -3; + -ms-flex-order: -4; + order: -4; +} + +.shaka-container .shaka-small-play-button { + -webkit-box-ordinal-group: -2; + -ms-flex-order: -3; + order: -3; +} + +.shaka-container .shaka-mute-button { + -webkit-box-ordinal-group: -1; + -ms-flex-order: -2; + order: -2; +} + +.shaka-container .shaka-controls-button-panel>* { + margin: 0; + padding: 3px 8px; + color: #EEE; + height: 40px; +} + +.shaka-container .shaka-controls-button-panel>*:hover { + color: #FFF; +} + +.shaka-container .shaka-controls-button-panel .shaka-volume-bar-container { + position: relative; + z-index: 10; + left: -1px; + -webkit-box-ordinal-group: 0; + -ms-flex-order: -1; + order: -1; + opacity: 0; + width: 0px; + -webkit-transition: width 0.2s cubic-bezier(0.4, 0, 1, 1); + height: 3px; + transition: width 0.2s cubic-bezier(0.4, 0, 1, 1); + padding: 0; +} + +.shaka-container .shaka-controls-button-panel .shaka-volume-bar-container:hover, +.shaka-container .shaka-controls-button-panel .shaka-volume-bar-container:focus { + display: block; + width: 50px; + opacity: 1; + padding: 0 6px; +} + +.shaka-container .shaka-mute-button:hover+div { + opacity: 1; + width: 50px; + padding: 0 6px; +} + +.shaka-container .shaka-current-time { + padding: 0 10px; + font-size: 12px; +} + +.shaka-container .shaka-seek-bar-container { + height: 3px; + position: relative; + top: -1px; + border-radius: 0; + margin-bottom: 0; +} + +.shaka-container .shaka-seek-bar-container .shaka-range-element { + opacity: 0; + transition: opacity 0.2s cubic-bezier(0.4, 0, 1, 1); +} + +.shaka-container .shaka-seek-bar-container:hover { + height: 5px; + top: 0; + cursor: pointer; +} + +.shaka-container .shaka-seek-bar-container:hover .shaka-range-element { + opacity: 1; + cursor: pointer; +} + +.shaka-container .shaka-seek-bar-container input[type=range]::-webkit-slider-thumb { + background: #FF0000; + cursor: pointer; +} + +.shaka-container .shaka-seek-bar-container input[type=range]::-moz-range-thumb { + background: #FF0000; + cursor: pointer; +} + +.shaka-container .shaka-seek-bar-container input[type=range]::-ms-thumb { + background: #FF0000; + cursor: pointer; +} + +.shaka-container .shaka-video-container * { + font-family: 'Roboto', sans-serif; +} + +.shaka-container .shaka-video-container .material-icons-round { + font-family: 'Material Icons Sharp'; +} + +.shaka-container .shaka-overflow-menu, +.shaka-container .shaka-settings-menu { + border-radius: 2px; + background: rgba(37, 37, 37, 0.9); + text-shadow: 0 0 2px rgb(0 0 0%); + -webkit-transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1); + transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1); + -moz-user-select: none; + -ms-user-select: none; + animation: fade 0.3s; + -webkit-user-select: none; + right: 10px; + bottom: 50px; + padding: 0; + min-width: 200px; +} + +@keyframes fade { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +.shaka-container .shaka-settings-menu { + padding: 0 0 8px; +} + +.shaka-container .shaka-settings-menu button { + font-size: 12px; +} + +.shaka-container .shaka-settings-menu button span { + margin-left: 33px; + font-size: 13px; +} + +.shaka-container .shaka-settings-menu button[aria-selected="true"] { + display: -webkit-box; + display: -ms-flexbox; + display: flex; +} + +.shaka-container .shaka-settings-menu button[aria-selected="true"] span { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + margin-left: 0; +} + +.shaka-container .shaka-settings-menu button[aria-selected="true"] i { + -webkit-box-ordinal-group: 2; + -ms-flex-order: 1; + order: 1; + font-size: 18px; + padding-left: 5px; +} + +.shaka-container .shaka-overflow-menu button { + padding: 0; +} + +.shaka-container .shaka-overflow-menu button i { + display: none; +} + +.shaka-container .shaka-overflow-menu button .shaka-overflow-button-label { + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + cursor: default; + outline: none; + height: 40px; + -webkit-box-flex: 0; + -ms-flex: 0 0 100%; + flex: 0 0 100%; +} + +.shaka-container .shaka-overflow-menu button .shaka-overflow-button-label span { + -ms-flex-negative: initial; + flex-shrink: initial; + padding-left: 15px; + font-size: 13px; + font-weight: 500; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.shaka-container .shaka-overflow-menu span+span { + color: #FFF; + font-weight: 400 !important; + font-size: 12px !important; + padding-right: 8px; + padding-left: 0 !important; +} + +.shaka-container .shaka-overflow-menu span+span:after { + content: "navigate_next"; + font-family: 'Material Icons Sharp'; + font-size: 20px; +} + +.shaka-container .shaka-overflow-menu .shaka-pip-button span+span { + padding-right: 15px !important; +} + +.shaka-container .shaka-overflow-menu .shaka-pip-button span+span:after { + content: ""; +} + +.shaka-container .shaka-back-to-overflow-button { + padding: 8px 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + font-size: 12px; + color: #eee; + height: 40px; +} + +.shaka-container .shaka-back-to-overflow-button .material-icons-round { + font-size: 15px; + padding-right: 10px; +} + +.shaka-container .shaka-back-to-overflow-button span { + margin-left: 3px !important; +} + +.shaka-container .shaka-overflow-menu button:hover, +.shaka-container .shaka-settings-menu button:hover { + background-color: rgba(255, 255, 255, 0.1); + cursor: pointer; +} + +.shaka-container .shaka-overflow-menu button:hover label, +.shaka-container .shaka-settings-menu button:hover label { + cursor: pointer; +} + +.shaka-container .shaka-overflow-menu button, +.shaka-container .shaka-settings-menu button { + color: #EEE; +} + +.shaka-container .shaka-captions-off { + color: #BFBFBF; +} + +.shaka-container .shaka-overflow-menu-button { + font-size: 18px; + margin-right: 5px; +} + +.shaka-container .shaka-fullscreen-button:hover { + font-size: 25px; + -webkit-transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1); + transition: font-size 0.1s cubic-bezier(0, 0, 0.2, 1); +} + +.shaka-container .shaka-overflow-menu, +.shaka-container .shaka-settings-menu { + border-radius: 10px; +} + +@media (prefers-color-scheme: light) { + + .shaka-container .shaka-overflow-menu, + .shaka-container .shaka-settings-menu { + background: rgba(255, 255, 255, 0.9); + } + + .shaka-container .shaka-overflow-menu span+span, + .shaka-container .shaka-overflow-menu button, + .shaka-container .shaka-settings-menu button { + color: #000000; + } +} + +@media (min-width: 800px) { + .shaka-container .shaka-controls-button-panel { + -webkit-box-ordinal-group: 3; + -ms-flex-order: 2; + order: 2; + height: 40px; + padding: 0 10px; + } +} + +@media (max-width: 800px) { + .shaka-container .shaka-scrim-container { + background: rgba(0, 0, 0, 0.5); + } + + .shaka-container .shaka-range-container { + margin: 0; + top: 0; + } + + .shaka-container .shaka-mute-button { + display: none; + } + + .shaka-container .shaka-overflow-menu, + .shaka-container .shaka-settings-menu { + bottom: 0; + top: 0; + left: 0; + right: 0; + width: 80%; + margin: 10px; + border-radius: 10px; + } + + .shaka-container .shaka-overflow-menu button, + .shaka-container .shaka-settings-menu button { + width: 100%; + height: 40px; + padding: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + } + + .shaka-container .shaka-overflow-menu button span, + .shaka-container .shaka-settings-menu button span { + margin-left: 0; + padding-left: 15px; + } +} \ No newline at end of file diff --git a/examples/browser/web/src/assets/style.css b/examples/browser/web/src/assets/style.css new file mode 100644 index 00000000..62f455a7 --- /dev/null +++ b/examples/browser/web/src/assets/style.css @@ -0,0 +1,135 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + background-color: #202020; + color: rgb(255, 255, 255); + line-height: 1.6; + font-family: Roboto, Arial, sans-serif; + font-size: 15px; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-tap-highlight-color: transparent; +} + +hr { + width: 100%; + border: 1px solid transparent; + background-color: rgb(68, 68, 68); +} + +form { + margin: 0.5rem 0; + display: none; + border-radius: 0.3rem; + background-color: rgb(68, 68, 68); +} + +form input { + padding: 0.5rem; + border: none; + color: rgb(255, 255, 255); +} + +form input[type="text"] { + background: transparent; +} + +form input[type="text"]:focus { + outline: none; +} + +form input[type="submit"] { + color: rgb(255, 255, 255); + background-color: rgba(0, 0, 0, 0.244); + cursor: pointer; +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +input:-webkit-autofill:active { + -webkit-background-clip: text; + -webkit-text-fill-color: #ffffff; + transition: background-color 5000s ease-in-out 0s; +} + +#loader { + display: block; + border: 10px solid rgb(68, 68, 68); + border-top: 10px solid rgb(255, 255, 255); + border-radius: 50%; + width: 50px; + height: 50px; + align-self: center; + animation: spin 1s linear infinite; + margin: 0.5rem 0; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +#shaka-container { + height: 40vw; +} + +#video-container { + display: none; + flex-direction: column; + position: relative; + width: 70vw !important; + margin: 0.5rem 0; +} + +#metadata { + display: flex; + flex-direction: row; + align-self: left; + margin: 0.5rem 0; +} + +#metadata>#metadata-item { + margin: 0 0.3rem; + background-color: #ffffff; + color: rgba(0, 0, 0, 0.757); + font-weight: 600; + padding: 0.2rem 0.5rem; + border-radius: 0.3rem; +} + +#video-container>#description { + align-self: left; + margin-left: 0.5rem; + font-size: medium; +} + +video { + width: 100%; + height: 100%; +} + +footer { + margin: 0.5rem 0; +} + +@media screen and (max-width: 768px) { + video { + height: auto; + } + + #shaka-container { + height: auto; + } + + #video-container { + width: 100% !important; + } +} \ No newline at end of file diff --git a/examples/browser/web/src/main.ts b/examples/browser/web/src/main.ts index cc185f45..7d1ce24e 100644 --- a/examples/browser/web/src/main.ts +++ b/examples/browser/web/src/main.ts @@ -1,27 +1,27 @@ -import './style.css'; import { Innertube, UniversalCache } from '../../../../bundle/browser'; -import dashjs from 'dashjs'; -const description = document.getElementById('description') as HTMLDivElement; -const form = document.querySelector('form') as HTMLFormElement; +// @ts-ignore - Shaka's TS support is not the best. +import shaka from 'shaka-player/dist/shaka-player.ui.js'; + +import "shaka-player/dist/controls.css"; + const title = document.getElementById('title') as HTMLHeadingElement; +const description = document.getElementById('description') as HTMLDivElement; const metadata = document.getElementById('metadata') as HTMLDivElement; const loader = document.getElementById('loader') as HTMLDivElement; -const video = document.getElementById('video') as HTMLVideoElement; -const video_container = document.getElementById('video_container') as HTMLDivElement; +const form = document.querySelector('form') as HTMLFormElement; async function main() { const yt = await Innertube.create({ generate_session_locally: true, fetch: async (input: RequestInfo | URL, init?: RequestInit) => { - // url const url = typeof input === 'string' ? new URL(input) : input instanceof URL ? input : new URL(input.url); - // transform the url for use with our proxy + // Transform the url for use with our proxy. url.searchParams.set('__host', url.host); url.host = 'localhost:8080'; url.protocol = 'http'; @@ -32,13 +32,15 @@ async function main() { ? input.headers : new Headers(); - // now serialize the headers + // Now serialize the headers. url.searchParams.set('__headers', JSON.stringify([...headers])); - // @ts-ignore - input.duplex = 'half'; + if (input instanceof Request) { + // @ts-ignore + input.duplex = 'half'; + } - // copy over the request + // Copy over the request. const request = new Request( url, input instanceof Request ? input : undefined, @@ -46,7 +48,6 @@ async function main() { headers.delete('user-agent'); - // fetch the url return fetch(request, init ? { ...init, headers @@ -60,45 +61,46 @@ async function main() { form.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' }); form.style.display = 'block'; - showUI(false); + showUI({ hidePlayer: true }); - let player: dashjs.MediaPlayerClass | undefined; + let player: shaka.Player | undefined; + let ui: shaka.ui.Overlay | undefined; form.addEventListener('submit', async (e) => { e.preventDefault(); if (player) { - player.reset(); + player.destroy(); } hideUI(); - let video_id; + let videoId; - const video_id_or_url = document.querySelector('input[type=text]')?.value; + const videoIdOrURL = document.querySelector('input[type=text]')?.value; - if (!video_id_or_url) { + if (!videoIdOrURL) { title.textContent = 'No video id or URL provided'; - showUI(false); + showUI({ hidePlayer: true }); return; } try { - if (video_id_or_url.match(/(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/)) { - const endpoint = await yt.resolveURL(video_id_or_url); + if (videoIdOrURL.match(/(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])/)) { + const endpoint = await yt.resolveURL(videoIdOrURL); if (!endpoint.payload.videoId) { title.textContent = 'Could not resolve URL'; - showUI(false); + showUI({ hidePlayer: true }); return; } - video_id = endpoint.payload.videoId; + videoId = endpoint.payload.videoId; } else { - video_id = video_id_or_url; + videoId = videoIdOrURL; } - const info = await yt.getInfo(video_id); + const info = await yt.getInfo(videoId); title.textContent = info.basic_info.title || null; description.innerHTML = info.secondary_info?.description.toHTML() || ''; @@ -106,12 +108,12 @@ async function main() { document.title = info.basic_info.title || ''; - metadata!.innerHTML = ''; - metadata!.innerHTML += ``; - metadata!.innerHTML += ``; - metadata!.innerHTML += ``; + metadata.innerHTML = ''; + metadata.innerHTML += `
${info.primary_info?.published.toHTML()}
`; + metadata.innerHTML += `
${info.primary_info?.view_count.toHTML()}
`; + metadata.innerHTML += `
${info.basic_info.like_count} likes
`; - showUI(true); + showUI({ hidePlayer: false }); const dash = await info.toDash((url) => { url.searchParams.set('__host', url.host); @@ -122,35 +124,155 @@ async function main() { const uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash); - // create and append video element - const video_element = document.querySelector('video') as HTMLVideoElement; - video_element.setAttribute('controls', 'true'); - video_element.poster = info.basic_info.thumbnail![0].url; - - // use dash.js to parse the manifest if (player) { - player.destroy(); + await player.destroy(); + player = undefined; } - player = dashjs.MediaPlayer().create(); - player.initialize(video_element, uri, true); - player.setInitialMediaSettingsFor('audio', { lang: 'en-US' }); + if (ui) { + ui.destroy(); + ui = undefined; + } + + const videoEl = document.getElementById('videoel') as HTMLVideoElement; + const shakaContainer = document.getElementById('shaka-container') as HTMLDivElement; + + shakaContainer + .querySelectorAll("div") + .forEach(node => node.remove()); + + shaka.polyfill.installAll(); + + if (shaka.Player.isBrowserSupported()) { + videoEl.poster = info.basic_info.thumbnail![0].url; + + player = new shaka.Player(videoEl); + ui = new shaka.ui.Overlay(player, shakaContainer, videoEl); + + const config = { + seekBarColors: { + base: 'rgba(255,255,255,.2)', + buffered: 'rgba(255,255,255,.4)', + played: 'rgb(255,0,0)', + }, + fadeDelay: 0, + }; + + ui.configure(config); + + const overflowMenuButton = document.querySelector('.shaka-overflow-menu-button'); + if (overflowMenuButton) { + overflowMenuButton.innerHTML = 'settings'; + } + + const backToOverflowButton = document.querySelector('.shaka-back-to-overflow-button .material-icons-round'); + if (backToOverflowButton) { + backToOverflowButton.innerHTML = 'arrow_back_ios_new'; + } + + player.configure({ + streaming: { + bufferingGoal: 180, + rebufferingGoal: 0.02, + bufferBehind: 300 + } + }); + + player.getNetworkingEngine()?.registerRequestFilter((_type: any, request: any) => { + const uri = request.uris[0]; + const url = new URL(uri); + const headers = request.headers; + + request.method = 'POST'; + + // protobuf - { 15: 0 } + request.body = new Uint8Array([120, 0]); + + if (url.pathname === "/videoplayback") { + if (headers.Range) { + request.headers = {}; + url.searchParams.set("range", headers.Range.split("=")[1]); + url.searchParams.set("alr", "yes"); + } + } + + request.uris[0] = url.toString(); + }); + + // The UTF-8 characters "h", "t", "t", and "p". + const HTTP_IN_HEX = 0x68747470; + + const RequestType = shaka.net.NetworkingEngine.RequestType; + + player.getNetworkingEngine()?.registerResponseFilter(async (type: any, response: any) => { + const dataView = new DataView(response.data); + + if (response.data.byteLength < 4 || + dataView.getUint32(0) != HTTP_IN_HEX) { + // This doesn't start with "http", so it is not an ALR. + return; + } + + // Interpret the response data as a URL string. + const response_as_string = shaka.util.StringUtils.fromUTF8(response.data); + + let retry_parameters; + + if (type == RequestType.MANIFEST) { + retry_parameters = player!.getConfiguration().manifest.retryParameters; + } else if (type == RequestType.SEGMENT) { + retry_parameters = player!.getConfiguration().streaming.retryParameters; + } else if (type == RequestType.LICENSE) { + retry_parameters = player!.getConfiguration().drm.retryParameters; + } else { + retry_parameters = shaka.net.NetworkingEngine.defaultRetryParameters(); + } + + // Make another request for the redirect URL. + const uris = [response_as_string]; + const redirect_request = shaka.net.NetworkingEngine.makeRequest(uris, retry_parameters); + const request_operation = player!.getNetworkingEngine()!.request(type, redirect_request); + const redirect_response = await request_operation.promise; + + // Modify the original response to contain the results of the redirect + // response. + response.data = redirect_response.data; + response.headers = redirect_response.headers; + response.uri = redirect_response.uri; + }); + + try { + await player.load(uri); + } catch (e) { + console.error('Could not load manifest', e); + } + } else { + console.error('Browser not supported!'); + } } catch (error) { title.textContent = 'An error occurred (see console)'; - showUI(false); + showUI({ hidePlayer: true }); console.error(error); } }); } -function showUI(with_video = true) { - loader.style.display = 'none'; - video.style.display = with_video ? 'block' : 'none'; +function showUI(args: { hidePlayer?: boolean } = { + hidePlayer: true, +}) { + const ytplayer = document.getElementById('shaka-container') as HTMLDivElement; + + ytplayer.style.display = args.hidePlayer ? 'none' : 'block'; + + const video_container = document.getElementById('video-container') as HTMLDivElement; video_container.animate({ opacity: [0, 1] }, { duration: 300, easing: 'ease-in-out' }); video_container.style.display = 'block'; + + loader.style.display = 'none'; } function hideUI() { + const video_container = document.getElementById('video-container') as HTMLDivElement; video_container.style.display = 'none'; loader.style.display = 'block'; } diff --git a/examples/browser/web/src/style.css b/examples/browser/web/src/style.css deleted file mode 100644 index 7acb2fd3..00000000 --- a/examples/browser/web/src/style.css +++ /dev/null @@ -1,90 +0,0 @@ -body { - display: flex; - flex-direction: column; - align-items: center; - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; - background-color: rgb(32, 32, 32); - color: rgb(255, 255, 255); -} - -hr { - height: 1px; - width: 100%; - border: 1px solid transparent; - background-color: rgb(68, 68, 68); -} - -form { - margin: 0.5rem 0; - display: none; -} - -#loader { - display: block; - border: 10px solid rgb(68, 68, 68); - border-top: 10px solid rgb(255, 255, 255); - border-radius: 50%; - width: 50px; - height: 50px; - align-self: center; - animation: spin 1s linear infinite; - margin: 0.5rem 0; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} - -#video_container { - display: none; - flex-direction: column; - position: relative; - width: 70vw !important; - margin: 0.5rem 0; -} - -#metadata { - display: flex; - flex-direction: row; - align-self: left; - margin: 0.5rem 0; -} - -#metadata > .metadata_item { - margin: 0 0.3rem; - background-color: beige; - color: black; - font: 1em bold; - padding: 0.2rem 0.5rem; - border-radius: 0.3rem; -} - -#video_container > #description { - align-self: left; - margin-left: 0.5rem; - font-size: medium; -} - -video { - width: 100%; - height: 40vw; -} - -footer { - margin: 0.5rem 0; -} - -@media screen and (max-width: 768px) { - video { - height: auto; - } - #video_container { - width: 100% !important; - } -} \ No newline at end of file diff --git a/src/parser/youtube/HashtagFeed.ts b/src/parser/youtube/HashtagFeed.ts index 81113830..7a6d278b 100644 --- a/src/parser/youtube/HashtagFeed.ts +++ b/src/parser/youtube/HashtagFeed.ts @@ -8,9 +8,10 @@ import type Actions from '../../core/Actions.js'; import type { ApiResponse } from '../../core/Actions.js'; import type ChipCloudChip from '../classes/ChipCloudChip.js'; import type { IBrowseResponse } from '../index.js'; +import { PageHeader } from '../nodes.js'; export default class HashtagFeed extends FilterableFeed { - header?: HashtagHeader; + header?: HashtagHeader | PageHeader; contents: RichGrid; constructor(actions: Actions, response: IBrowseResponse | ApiResponse) { @@ -25,7 +26,7 @@ export default class HashtagFeed extends FilterableFeed { throw new InnertubeError('Content tab has no content', tab); if (this.page.header) { - this.header = this.page.header.item().as(HashtagHeader); + this.header = this.page.header.item().as(HashtagHeader, PageHeader); } this.contents = tab.content.as(RichGrid);