diff --git a/packages/app/src/pages/tv/live/[id]/chat/index.jsx b/packages/app/src/pages/tv/live/[id]/chat/index.jsx new file mode 100644 index 00000000..dec7fbe0 --- /dev/null +++ b/packages/app/src/pages/tv/live/[id]/chat/index.jsx @@ -0,0 +1,7 @@ +import React from "react" + +const ChatPage = () => { + return
Chat
+} + +export default ChatPage diff --git a/packages/app/src/pages/tv/live/decoders/hls.js b/packages/app/src/pages/tv/live/[id]/decoders/hls.js similarity index 97% rename from packages/app/src/pages/tv/live/decoders/hls.js rename to packages/app/src/pages/tv/live/[id]/decoders/hls.js index 980ee06a..c614691a 100644 --- a/packages/app/src/pages/tv/live/decoders/hls.js +++ b/packages/app/src/pages/tv/live/[id]/decoders/hls.js @@ -40,7 +40,6 @@ export default (player, sources = {}, options = {}) => { source += `?token=${options.authToken}` } - console.log("[HLS] Instance options >", options) console.log(`[HLS] Loading source [${source}]`) hlsInstance.attachMedia(player) diff --git a/packages/app/src/pages/tv/live/[id]/decoders/index.js b/packages/app/src/pages/tv/live/[id]/decoders/index.js new file mode 100644 index 00000000..fd2669a3 --- /dev/null +++ b/packages/app/src/pages/tv/live/[id]/decoders/index.js @@ -0,0 +1,2 @@ +export { default as hls } from "./hls" +export { default as shaka } from "./shaka" diff --git a/packages/app/src/pages/tv/live/[id]/decoders/shaka.js b/packages/app/src/pages/tv/live/[id]/decoders/shaka.js new file mode 100644 index 00000000..9d5e79dd --- /dev/null +++ b/packages/app/src/pages/tv/live/[id]/decoders/shaka.js @@ -0,0 +1,100 @@ +import shaka from "shaka-player" + +export default async (player, sources = {}, options = {}) => { + if (!player) { + console.error("[Shaka] player is not defined") + return false + } + + if (!sources.hls) { + console.error("[Shaka] an hls source is not provided") + return false + } + + let source = sources.hls + + // Initialize shaka player + const shakaInstance = new shaka.Player(player) + + // Helper function to sync to live edge + const syncToLive = () => { + if (shakaInstance.isLive()) { + const end = shakaInstance.seekRange().end + player.currentTime = end + } + } + + // Configure for low-latency HLS + shakaInstance.configure({ + streaming: { + lowLatencyMode: true, + inaccurateManifestTolerance: 0, + rebufferingGoal: 0.01, + bufferingGoal: 0.1, + bufferBehind: 30, + startAtSegmentBoundary: false, + durationBackoff: 0.2, + }, + }) + + // Add request filter for authentication if token is provided + if (options.authToken) { + shakaInstance + .getNetworkingEngine() + .registerRequestFilter((type, request) => { + request.headers = { + ...request.headers, + Authorization: `Bearer ${options.authToken}`, + } + }) + source += `?token=${options.authToken}` + } + + console.log("[Shaka] Instance options >", options) + console.log(`[Shaka] Loading source [${source}]`) + + // Error handling + shakaInstance.addEventListener("error", (error) => { + console.error("[Shaka] Error", error) + }) + + // Buffer state monitoring + player.addEventListener("waiting", () => { + console.log("[Shaka] Buffer underrun") + }) + + // Handle stream end + player.addEventListener("ended", () => { + console.log("[Shaka] Stream ended") + if (typeof options.onSourceEnd === "function") { + options.onSourceEnd() + } + }) + + try { + await shakaInstance.load(source) + console.log("[Shaka] Stream loaded successfully") + + const tracks = shakaInstance.getVariantTracks() + console.log("[Shaka] Available qualities >", tracks) + } catch (error) { + console.error("[Shaka] Error loading stream:", error) + } + + player.addEventListener("play", () => { + console.log("[SHAKA] Syncing to last position") + syncToLive() + }) + + // Add destroy method for cleanup + shakaInstance._destroy = () => { + try { + shakaInstance.unload() + shakaInstance.destroy() + } catch (error) { + console.error("[Shaka] Error during cleanup:", error) + } + } + + return shakaInstance +} diff --git a/packages/app/src/pages/tv/live/[id].jsx b/packages/app/src/pages/tv/live/[id]/index.jsx similarity index 96% rename from packages/app/src/pages/tv/live/[id].jsx rename to packages/app/src/pages/tv/live/[id]/index.jsx index aee875c2..a4afd8df 100755 --- a/packages/app/src/pages/tv/live/[id].jsx +++ b/packages/app/src/pages/tv/live/[id]/index.jsx @@ -29,7 +29,7 @@ async function fetchStream(stream_id) { stream = stream[0] } - if (!stream.sources) { + if (!stream.sources || !stream.sources.hls) { return false } @@ -60,6 +60,8 @@ export default class StreamViewer extends React.Component { return false } + console.log(`[TV] Switching decoder to: ${decoder}`) + await this.toggleLoading(true) // check if decoder is already loaded @@ -71,8 +73,6 @@ export default class StreamViewer extends React.Component { this.setState({ decoderInstance: null }) } - console.log(`[TV] Switching decoder to: ${decoder}`) - const decoderInstance = await Decoders[decoder](...args) await this.setState({ @@ -236,8 +236,12 @@ export default class StreamViewer extends React.Component { spectators: stream.viewers, }) - // joinStreamWebsocket - await this.joinStreamWebsocket(stream) + try { + // joinStreamWebsocket + this.joinStreamWebsocket(stream) + } catch (error) { + console.error(error) + } // load decoder with provided data await this.loadDecoder( @@ -260,6 +264,10 @@ export default class StreamViewer extends React.Component { this.state.decoderInstance.destroy() } + if (typeof this.state.decoderInstance?._destroy === "function") { + this.state.decoderInstance._destroy() + } + if (this.state.websocket) { if (typeof this.state.websocket.destroy === "function") { this.state.websocket.destroy() diff --git a/packages/app/src/pages/tv/live/index.less b/packages/app/src/pages/tv/live/[id]/index.less similarity index 100% rename from packages/app/src/pages/tv/live/index.less rename to packages/app/src/pages/tv/live/[id]/index.less diff --git a/packages/app/src/pages/tv/live/decoders/flv.js b/packages/app/src/pages/tv/live/decoders/flv.js deleted file mode 100644 index 89571be0..00000000 --- a/packages/app/src/pages/tv/live/decoders/flv.js +++ /dev/null @@ -1,29 +0,0 @@ -import mpegts from "mpegts.js" - -export default async (player, source, { onSourceEnd } = {}) => { - if (!source) { - console.error("Stream source is not defined") - return false - } - - const decoderInstance = mpegts.createPlayer({ - type: "flv", - isLive: true, - enableWorker: true, - url: source, - }) - - if (typeof onSourceEnd === "function") { - decoderInstance.on(mpegts.Events.ERROR, onSourceEnd) - } - - decoderInstance.attachMediaElement(player) - - decoderInstance.load() - - await decoderInstance.play().catch((error) => { - console.error(error) - }) - - return decoderInstance -} diff --git a/packages/app/src/pages/tv/live/decoders/index.js b/packages/app/src/pages/tv/live/decoders/index.js deleted file mode 100644 index f5d88df4..00000000 --- a/packages/app/src/pages/tv/live/decoders/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as hls } from "./hls" -export { default as flv } from "./flv"