diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd44461..b514da6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for casting clips from web app +- A workaround for video loading errors: uses Playlet Web app and [YouTube.js](https://github.com/LuanRT/YouTube.js) to load streaming data. This workaround is limited, and might not work. It doesn't have closed captions, nor trickplay thumbnails (storyboards). + - **How To use**: Open the web app, click on a video, and choose "Play on (HOT FIX)". ## [0.25.6] - 2024-09-12 diff --git a/playlet-lib/src/components/Web/PlayletWebServer/Middleware/ProxyRouter.bs b/playlet-lib/src/components/Web/PlayletWebServer/Middleware/ProxyRouter.bs new file mode 100644 index 00000000..a4596669 --- /dev/null +++ b/playlet-lib/src/components/Web/PlayletWebServer/Middleware/ProxyRouter.bs @@ -0,0 +1,69 @@ +import "pkg:/source/services/HttpClient.bs" + +namespace Http + + class ProxyRouter extends HttpRouter + + function new(server as object) + super() + + task = server.task + m.invidiousNode = task.invidious + m.invidiousService = new Invidious.InvidiousService(m.invidiousNode) + end function + + @post("/api/proxy") + function ProxyRequest(context as object) as boolean + request = context.request + response = context.response + + requestArgs = request.Json() + if requestArgs = invalid + response.Default(400, "Invalid request body") + return true + end if + + httpReq = HttpClient.FromObject(requestArgs) + httpRes = httpReq.Await() + + result = { + "status": httpRes.StatusCode() + "headers": httpRes.Headers() + "body": httpRes.Text() + } + + response.Json(result) + return true + end function + + @post("/api/ytjs-cache") + function CacheVideoInfo(context as object) as boolean + request = context.request + response = context.response + + json = request.Json() + if json = invalid + response.Default(400, "Invalid request body") + return true + end if + + instance = m.invidiousService.GetInstance() + videoId = json.videoId + + url = `${instance}${Invidious.VIDEOS_ENDPOINT}/${videoId}` + + ' Place a fake cache so we can read it later + requestObj = HttpClient.Get(url) + requestObj.CacheSeconds(18000) + responseObj = new HttpClient.HttpResponse(requestObj, invalid) + responseObj.OverrideStatusCode(200) + responseObj.OverrideText(request.body) + HttpClientCache.Set(responseObj) + + response.Default(204, "OK") + return true + end function + + end class + +end namespace diff --git a/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs b/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs index 69f12cb7..3a5cb5ab 100644 --- a/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs +++ b/playlet-lib/src/components/Web/PlayletWebServer/PlayletWebServer.bs @@ -9,6 +9,7 @@ import "pkg:/components/Web/PlayletWebServer/Middleware/InvidiousRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/PlayletLibUrlsRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/PreferencesRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/ProfilesRouter.bs" +import "pkg:/components/Web/PlayletWebServer/Middleware/ProxyRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/RegistryRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/SearchHistoryRouter.bs" import "pkg:/components/Web/PlayletWebServer/Middleware/StateApiRouter.bs" @@ -52,6 +53,7 @@ function SetupRoutes(server as object) server.UseRouter(new Http.PlayletLibUrlsRouter(server)) server.UseRouter(new Http.DialRouter(server)) server.UseRouter(new Http.CacheRouter()) + server.UseRouter(new Http.ProxyRouter(server)) etags = new Http.EtagUtils() server.UseRouter(new Http.HttpStaticFilesRouter("/", "libpkg:/www", etags, { staticFiles: true })) diff --git a/playlet-lib/src/source/services/HttpClient.bs b/playlet-lib/src/source/services/HttpClient.bs index f5f73345..64f5975d 100644 --- a/playlet-lib/src/source/services/HttpClient.bs +++ b/playlet-lib/src/source/services/HttpClient.bs @@ -119,6 +119,20 @@ namespace HttpClient return (new HttpRequest()).Method("HEAD").Url(url) end function + function FromObject(obj as object) as HttpRequest + request = new HttpRequest() + + for each key in obj + if IsFunction(request[key]) + request[key](obj[key]) + else + throw `Invalid key: "${key}" in object passed to HttpClient.FromObject` + end if + end for + + return request + end function + class HttpRequest public urlTransfer as object @@ -598,6 +612,10 @@ namespace HttpClient m._statusCode = statusCode end function + function OverrideText(text as string) + m._text = text + end function + function IsSuccess() as boolean statusCode = m.StatusCode() return statusCode >= 200 and statusCode < 400 diff --git a/playlet-web/package-lock.json b/playlet-web/package-lock.json index 4ceddfac..e13eadcf 100644 --- a/playlet-web/package-lock.json +++ b/playlet-web/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "playlet-web", "version": "0.25.6", + "dependencies": { + "youtubei.js": "^10.5.0" + }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tailwindcss/typography": "^0.5.15", @@ -1816,6 +1819,11 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.1.0.tgz", + "integrity": "sha512-+2Mx67Y3skJ4NCD/qNSdBJNWtu6x6Qr53jeNg+QcwiL6mt0wK+3jwHH2x1p7xaYH6Ve2JKOVn0OxU35WsmqI9A==" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -2184,6 +2192,14 @@ "node": ">=12" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -2601,7 +2617,6 @@ "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3423,6 +3438,17 @@ "@types/estree": "*" } }, + "node_modules/jintr": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/jintr/-/jintr-2.1.1.tgz", + "integrity": "sha512-89cwX4ouogeDGOBsEVsVYsnWWvWjchmwXBB4kiBhmjOKw19FiOKhNhMhpxhTlK2ctl7DS+d/ethfmuBpzoNNgA==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "dependencies": { + "acorn": "^8.8.0" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -4546,7 +4572,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "dev": true, "license": "0BSD" }, "node_modules/typescript": { @@ -4563,6 +4588,17 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -4759,6 +4795,20 @@ "engines": { "node": ">= 14" } + }, + "node_modules/youtubei.js": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-10.5.0.tgz", + "integrity": "sha512-iyA+VF28c15tCCKH9ExM2RKC3zYiHzA/eixGlJ3vERANkuI+xYKzAZ4vtOhmyqwrAddu88R/DkzEsmpph5NWjg==", + "funding": [ + "https://github.com/sponsors/LuanRT" + ], + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "jintr": "^2.1.1", + "tslib": "^2.5.0", + "undici": "^5.19.1" + } } } } diff --git a/playlet-web/package.json b/playlet-web/package.json index 44d77232..02abfe43 100644 --- a/playlet-web/package.json +++ b/playlet-web/package.json @@ -27,5 +27,8 @@ "typescript": "^5.6.2", "vite": "^5.4.7", "vite-tsconfig-paths": "^5.0.1" + }, + "dependencies": { + "youtubei.js": "^10.5.0" } } diff --git a/playlet-web/src/lib/Api/YoutubeJs.ts b/playlet-web/src/lib/Api/YoutubeJs.ts new file mode 100644 index 00000000..db0c643a --- /dev/null +++ b/playlet-web/src/lib/Api/YoutubeJs.ts @@ -0,0 +1,287 @@ +import { Innertube } from 'youtubei.js/web'; +import { getHost } from "lib/Api/Host"; + +// https://github.com/iv-org/invidious/blob/a021b93063f3956fc9bb3cce0fb56ea252422738/src/invidious/videos/formats.cr#L7 +const FORMATS = { + "5": { ext: "flv", width: 400, height: 240, acodec: "mp3", abr: 64, vcodec: "h263" }, + "6": { ext: "flv", width: 450, height: 270, acodec: "mp3", abr: 64, vcodec: "h263" }, + "13": { ext: "3gp", acodec: "aac", vcodec: "mp4v" }, + "17": { ext: "3gp", width: 176, height: 144, acodec: "aac", abr: 24, vcodec: "mp4v" }, + "18": { ext: "mp4", width: 640, height: 360, acodec: "aac", abr: 96, vcodec: "h264" }, + "22": { ext: "mp4", width: 1280, height: 720, acodec: "aac", abr: 192, vcodec: "h264" }, + "34": { ext: "flv", width: 640, height: 360, acodec: "aac", abr: 128, vcodec: "h264" }, + "35": { ext: "flv", width: 854, height: 480, acodec: "aac", abr: 128, vcodec: "h264" }, + "36": { ext: "3gp", width: 320, acodec: "aac", vcodec: "mp4v" }, + "37": { ext: "mp4", width: 1920, height: 1080, acodec: "aac", abr: 192, vcodec: "h264" }, + "38": { ext: "mp4", width: 4096, height: 3072, acodec: "aac", abr: 192, vcodec: "h264" }, + "43": { ext: "webm", width: 640, height: 360, acodec: "vorbis", abr: 128, vcodec: "vp8" }, + "44": { ext: "webm", width: 854, height: 480, acodec: "vorbis", abr: 128, vcodec: "vp8" }, + "45": { ext: "webm", width: 1280, height: 720, acodec: "vorbis", abr: 192, vcodec: "vp8" }, + "46": { ext: "webm", width: 1920, height: 1080, acodec: "vorbis", abr: 192, vcodec: "vp8" }, + "59": { ext: "mp4", width: 854, height: 480, acodec: "aac", abr: 128, vcodec: "h264" }, + "78": { ext: "mp4", width: 854, height: 480, acodec: "aac", abr: 128, vcodec: "h264" }, + "82": { ext: "mp4", height: 360, format: "3D", acodec: "aac", abr: 128, vcodec: "h264" }, + "83": { ext: "mp4", height: 480, format: "3D", acodec: "aac", abr: 128, vcodec: "h264" }, + "84": { ext: "mp4", height: 720, format: "3D", acodec: "aac", abr: 192, vcodec: "h264" }, + "85": { ext: "mp4", height: 1080, format: "3D", acodec: "aac", abr: 192, vcodec: "h264" }, + "100": { ext: "webm", height: 360, format: "3D", acodec: "vorbis", abr: 128, vcodec: "vp8" }, + "101": { ext: "webm", height: 480, format: "3D", acodec: "vorbis", abr: 192, vcodec: "vp8" }, + "102": { ext: "webm", height: 720, format: "3D", acodec: "vorbis", abr: 192, vcodec: "vp8" }, + "91": { ext: "mp4", height: 144, format: "HLS", acodec: "aac", abr: 48, vcodec: "h264" }, + "92": { ext: "mp4", height: 240, format: "HLS", acodec: "aac", abr: 48, vcodec: "h264" }, + "93": { ext: "mp4", height: 360, format: "HLS", acodec: "aac", abr: 128, vcodec: "h264" }, + "94": { ext: "mp4", height: 480, format: "HLS", acodec: "aac", abr: 128, vcodec: "h264" }, + "95": { ext: "mp4", height: 720, format: "HLS", acodec: "aac", abr: 256, vcodec: "h264" }, + "96": { ext: "mp4", height: 1080, format: "HLS", acodec: "aac", abr: 256, vcodec: "h264" }, + "132": { ext: "mp4", height: 240, format: "HLS", acodec: "aac", abr: 48, vcodec: "h264" }, + "151": { ext: "mp4", height: 72, format: "HLS", acodec: "aac", abr: 24, vcodec: "h264" }, + "133": { ext: "mp4", height: 240, format: "DASH video", vcodec: "h264" }, + "134": { ext: "mp4", height: 360, format: "DASH video", vcodec: "h264" }, + "135": { ext: "mp4", height: 480, format: "DASH video", vcodec: "h264" }, + "136": { ext: "mp4", height: 720, format: "DASH video", vcodec: "h264" }, + "137": { ext: "mp4", height: 1080, format: "DASH video", vcodec: "h264" }, + "138": { ext: "mp4", format: "DASH video", vcodec: "h264" }, + "160": { ext: "mp4", height: 144, format: "DASH video", vcodec: "h264" }, + "212": { ext: "mp4", height: 480, format: "DASH video", vcodec: "h264" }, + "264": { ext: "mp4", height: 1440, format: "DASH video", vcodec: "h264" }, + "298": { ext: "mp4", height: 720, format: "DASH video", vcodec: "h264", fps: 60 }, + "299": { ext: "mp4", height: 1080, format: "DASH video", vcodec: "h264", fps: 60 }, + "266": { ext: "mp4", height: 2160, format: "DASH video", vcodec: "h264" }, + "139": { ext: "m4a", format: "DASH audio", acodec: "aac", abr: 48, container: "m4a_dash" }, + "140": { ext: "m4a", format: "DASH audio", acodec: "aac", abr: 128, container: "m4a_dash" }, + "141": { ext: "m4a", format: "DASH audio", acodec: "aac", abr: 256, container: "m4a_dash" }, + "256": { ext: "m4a", format: "DASH audio", acodec: "aac", container: "m4a_dash" }, + "258": { ext: "m4a", format: "DASH audio", acodec: "aac", container: "m4a_dash" }, + "325": { ext: "m4a", format: "DASH audio", acodec: "dtse", container: "m4a_dash" }, + "328": { ext: "m4a", format: "DASH audio", acodec: "ec-3", container: "m4a_dash" }, + "167": { ext: "webm", height: 360, width: 640, format: "DASH video", container: "webm", vcodec: "vp8" }, + "168": { ext: "webm", height: 480, width: 854, format: "DASH video", container: "webm", vcodec: "vp8" }, + "169": { ext: "webm", height: 720, width: 1280, format: "DASH video", container: "webm", vcodec: "vp8" }, + "170": { ext: "webm", height: 1080, width: 1920, format: "DASH video", container: "webm", vcodec: "vp8" }, + "218": { ext: "webm", height: 480, width: 854, format: "DASH video", container: "webm", vcodec: "vp8" }, + "219": { ext: "webm", height: 480, width: 854, format: "DASH video", container: "webm", vcodec: "vp8" }, + "278": { ext: "webm", height: 144, format: "DASH video", container: "webm", vcodec: "vp9" }, + "242": { ext: "webm", height: 240, format: "DASH video", vcodec: "vp9" }, + "243": { ext: "webm", height: 360, format: "DASH video", vcodec: "vp9" }, + "244": { ext: "webm", height: 480, format: "DASH video", vcodec: "vp9" }, + "245": { ext: "webm", height: 480, format: "DASH video", vcodec: "vp9" }, + "246": { ext: "webm", height: 480, format: "DASH video", vcodec: "vp9" }, + "247": { ext: "webm", height: 720, format: "DASH video", vcodec: "vp9" }, + "248": { ext: "webm", height: 1080, format: "DASH video", vcodec: "vp9" }, + "271": { ext: "webm", height: 1440, format: "DASH video", vcodec: "vp9" }, + "272": { ext: "webm", height: 2160, format: "DASH video", vcodec: "vp9" }, + "302": { ext: "webm", height: 720, format: "DASH video", vcodec: "vp9", fps: 60 }, + "303": { ext: "webm", height: 1080, format: "DASH video", vcodec: "vp9", fps: 60 }, + "308": { ext: "webm", height: 1440, format: "DASH video", vcodec: "vp9", fps: 60 }, + "313": { ext: "webm", height: 2160, format: "DASH video", vcodec: "vp9" }, + "315": { ext: "webm", height: 2160, format: "DASH video", vcodec: "vp9", fps: 60 }, + "330": { ext: "webm", height: 144, format: "DASH video", vcodec: "vp9", fps: 60 }, + "331": { ext: "webm", height: 240, format: "DASH video", vcodec: "vp9", fps: 60 }, + "332": { ext: "webm", height: 360, format: "DASH video", vcodec: "vp9", fps: 60 }, + "333": { ext: "webm", height: 480, format: "DASH video", vcodec: "vp9", fps: 60 }, + "334": { ext: "webm", height: 720, format: "DASH video", vcodec: "vp9", fps: 60 }, + "335": { ext: "webm", height: 1080, format: "DASH video", vcodec: "vp9", fps: 60 }, + "336": { ext: "webm", height: 1440, format: "DASH video", vcodec: "vp9", fps: 60 }, + "337": { ext: "webm", height: 2160, format: "DASH video", vcodec: "vp9", fps: 60 }, + "171": { ext: "webm", acodec: "vorbis", format: "DASH audio", abr: 128 }, + "172": { ext: "webm", acodec: "vorbis", format: "DASH audio", abr: 256 }, + "249": { ext: "webm", format: "DASH audio", acodec: "opus", abr: 50 }, + "250": { ext: "webm", format: "DASH audio", acodec: "opus", abr: 70 }, + "251": { ext: "webm", format: "DASH audio", acodec: "opus", abr: 160 }, + "394": { ext: "mp4", height: 144, vcodec: "av01.0.05M.08" }, + "395": { ext: "mp4", height: 240, vcodec: "av01.0.05M.08" }, + "396": { ext: "mp4", height: 360, vcodec: "av01.0.05M.08" }, + "397": { ext: "mp4", height: 480, vcodec: "av01.0.05M.08" } +} + +export class YoutubeJs { + static host = () => `http://${getHost()}` + + static innerTube: Innertube; + static initPromise: Promise; + + static async init() { + if (YoutubeJs.innerTube) { + return; + } + + if (YoutubeJs.initPromise) { + return YoutubeJs.initPromise; + } + + YoutubeJs.initPromise = new Promise(async (resolve, reject) => { + try { + YoutubeJs.innerTube = await Innertube.create({ + // @ts-ignore + fetch: async (input: RequestInfo | URL, init?: RequestInit) => { + + const method = init?.method + ? init.method + : input instanceof Request + ? input.method + : 'GET'; + + const url = typeof input === 'string' + ? new URL(input) + : input instanceof URL + ? input + : new URL(input.url); + + const headers = init?.headers + ? new Headers(init.headers) + : input instanceof Request + ? input.headers + : new Headers(); + + const headersObject = {}; + headers.forEach((value, key) => { + headersObject[key] = value; + }); + + const body = init?.body; + + const args = { + Method: method, + Url: url, + Headers: headersObject, + Body: body, + CacheSeconds: -1, + }; + + const response = await fetch(`http://${getHost()}/api/proxy`, { + headers: { + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(args) + }) + + const responseData = await response.json(); + + return new Response(responseData.body, { + status: responseData.status, + statusText: `${responseData.status}`, + headers: responseData.headers, + }); + } + }); + resolve(); + } catch (error) { + console.error(error); + YoutubeJs.innerTube = null; + reject(error); + } + YoutubeJs.initPromise = null; + }); + await YoutubeJs.initPromise; + } + + static async getVideoInfo(videoId: string) { + await this.init(); + const info = await YoutubeJs.innerTube.getBasicInfo(videoId, 'ANDROID'); + + const formatStreams = info.streaming_data.formats.map(format => { + return { + url: format.decipher(YoutubeJs.innerTube.session.player), + itag: `${format.itag}`, + } + }); + + const adaptiveFormats = info.streaming_data.adaptive_formats.map(format => { + const formatInfo = FORMATS[format.itag]; + const result: any = { + init: format.init_range ? `${format.init_range.start}-${format.init_range.end}` : "", + index: format.index_range ? `${format.index_range.start}-${format.index_range.end}` : "", + bitrate: `${format.bitrate}`, + url: format.decipher(YoutubeJs.innerTube.session.player), + itag: `${format.itag}`, + type: format.mime_type, + clen: `${format.approx_duration_ms}`, + lmt: `${format.last_modified}`, + container: formatInfo?.ext, + encoding: formatInfo?.acodec ?? formatInfo?.vcodec, + }; + if (format.audio_quality) { + result.audioQuality = format.audio_quality; + } + if (format.audio_sample_rate) { + result.audioSampleRate = format.audio_sample_rate; + } + if (format.audio_channels) { + result.audioChannels = format.audio_channels; + } + + if (format.quality_label) { + result.qualityLabel = format.quality_label; + } + if (format.fps) { + result.fps = format.fps; + } + if (format.height && format.width) { + result.size = `${format.width}x${format.height}`; + result.resolution = `${format.height}p`; + } else if (formatInfo?.height && formatInfo?.width) { + result.size = `${formatInfo.width}x${formatInfo.height}`; + result.resolution = `${formatInfo.height}p`; + } + + return result; + }); + + // Populate a video object that is similar to Invidious format. + // Mostly populate only fields we care about, enough to make it work. + return { + type: "video", + title: info.basic_info.title, + videoId: info.basic_info.id, + videoThumbnails: [], + storyboards: [], + description: "", + published: 0, + publishedText: "", + keywords: [], + viewCount: 0, + likeCount: 0, + dislikeCount: 0, + paid: false, + premium: false, + isFamilyFriendly: true, + allowedRegions: [], + genre: "", + genreUrl: null, + author: info.basic_info.author, + authorId: info.basic_info.channel_id, + authorUrl: "", + authorVerified: false, + authorThumbnails: [], + subCountText: "", + lengthSeconds: info.basic_info.duration, + allowRatings: true, + rating: 0, + isListed: true, + liveNow: info.basic_info.is_live, + isPostLiveDvr: info.basic_info.is_post_live_dvr, + isUpcoming: info.basic_info.is_upcoming, + dashUrl: "", + hlsUrl: info.streaming_data.hls_manifest_url, + adaptiveFormats, + formatStreams, + captions: [], + recommendedVideos: [], + } + } + + static async postCacheData(data) { + await YoutubeJs.postJson(`${YoutubeJs.host()}/api/ytjs-cache`, data); + } + + private static postJson(url, payload) { + return fetch(url, { + headers: { + 'Content-Type': 'application/json' + }, + method: "POST", + body: JSON.stringify(payload) + }) + } +} diff --git a/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte b/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte index cb69be0e..9c27f75b 100644 --- a/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte +++ b/playlet-web/src/lib/VideoFeed/VideoCastDialog.svelte @@ -3,6 +3,7 @@ import { playletStateStore, tr } from "lib/Stores"; import VideoStartAt from "./VideoStartAt.svelte"; import VideoThumbnail from "./VideoThumbnail.svelte"; + import { YoutubeJs } from "lib/Api/YoutubeJs"; export let videoId: string | undefined = undefined; export let title: string | undefined = undefined; @@ -43,6 +44,17 @@ await PlayletApi.playVideo(getVideoInfo()); } + async function playOnTvHotFix() { + // measure length of time it takes to get video info + const start = performance.now(); + const videoInfo = await YoutubeJs.getVideoInfo(videoId); + const end = performance.now(); + console.log(`Time to get video info: ${end - start}ms`); + + await YoutubeJs.postCacheData(videoInfo); + await playOnTv(); + } + async function queueOnTv() { await PlayletApi.queueVideo(getVideoInfo()); } @@ -114,6 +126,12 @@ +