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"