A Kick channel is a stream as much as a chat, and moho shows only the chat: the header says what is on air, how many people are watching and for how long, but the video itself is somewhere else, in a browser tab beside the window that has the conversation in it.
This is not a player of its own. It is the same video surface a Discord DM video call and a Matrix group call will draw into - one pane, one pop-out, one picture-in-picture, one set of controls - with Kick as a third source feeding it.
Blocked on, and deliberately so:#14 (Discord screen share, hosting and viewing) and #81 (Matrix voice and video calls). Those two settle the hard questions - where real-time media lives, how a call is laid out, what the controls are, whether audio comes through the daemon or the renderer - and Kick is the easy source that should adopt those answers rather than invent its own and be rewritten later. Do not start this one first.
What Kick serves, measured rather than assumed.GET /api/v2/channels/{slug} already returns a playback_url, which the daemon parses past today:
That is Amazon IVS. It fetches anonymously - 200, application/vnd.apple.mpegurl, no bearer token, the signed token is already in the query string - and it is a standard HLS multivariant playlist, not LL-HLS. A live channel offered five renditions: 1080p60 at 5.8 Mbps, 720p60 at 3.4, 480p30 at 1.4, 360p30 at 630 kbps and 160p30 at 230 kbps.
Why the transport cannot be shared even though the surface can. Kick is one-to-many HLS over a CDN and needs an MSE player - hls.js, or Amazon's own IVS web SDK if the latency matters. Discord and Matrix screenshare are real-time media and arrive as a MediaStream attached to srcObject, which never touches MSE. So the reusable part is everything above the pipe: the surface, the layout, the pop-out, the sizing, and the <video> element itself. The shape that fits is one video surface with two source adapters - { kind: 'hls', url } and { kind: 'stream', stream }.
Done looks like: opening a live Kick channel offers to play it in the same pane a call uses; the picture and the chat are in one window; closing the pane leaves the chat where it was.
Notes for whoever picks this up:
playback_url belongs on the existing kickStream event, next to the title, category and viewer count. Every frontend that shows video needs it, and the 60-second refresh already running is what will replace an expired token.
Treat a 403 from a segment as "ask the daemon to refresh this channel", not as a dead stream.
The renderer's CSP needs media-src/connect-src for *.live-video.net. IVS sends permissive CORS, so no proxy is needed.
Default the rendition below 1080p60: a 5.8 Mbps 60fps decode beside a chat client is real CPU, and the ladder above gives a cheaper choice.
Electron's official builds carry H.264/AAC, which is what these renditions are.
A Kick channel is a stream as much as a chat, and moho shows only the chat: the header says what is on air, how many people are watching and for how long, but the video itself is somewhere else, in a browser tab beside the window that has the conversation in it.
This is not a player of its own. It is the *same* video surface a Discord DM video call and a Matrix group call will draw into - one pane, one pop-out, one picture-in-picture, one set of controls - with Kick as a third source feeding it.
**Blocked on, and deliberately so:** #14 (Discord screen share, hosting and viewing) and #81 (Matrix voice and video calls). Those two settle the hard questions - where real-time media lives, how a call is laid out, what the controls are, whether audio comes through the daemon or the renderer - and Kick is the easy source that should adopt those answers rather than invent its own and be rewritten later. Do not start this one first.
**What Kick serves, measured rather than assumed.** `GET /api/v2/channels/{slug}` already returns a `playback_url`, which the daemon parses past today:
```
https://<host>.us-west-2.playback.live-video.net/api/video/v1/us-west-2.<account>.channel.<id>.m3u8?token=...
```
That is Amazon IVS. It fetches anonymously - 200, `application/vnd.apple.mpegurl`, no bearer token, the signed token is already in the query string - and it is a standard HLS multivariant playlist, not LL-HLS. A live channel offered five renditions: 1080p60 at 5.8 Mbps, 720p60 at 3.4, 480p30 at 1.4, 360p30 at 630 kbps and 160p30 at 230 kbps.
**Why the transport cannot be shared even though the surface can.** Kick is one-to-many HLS over a CDN and needs an MSE player - hls.js, or Amazon's own IVS web SDK if the latency matters. Discord and Matrix screenshare are real-time media and arrive as a `MediaStream` attached to `srcObject`, which never touches MSE. So the reusable part is everything above the pipe: the surface, the layout, the pop-out, the sizing, and the `<video>` element itself. The shape that fits is one video surface with two source adapters - `{ kind: 'hls', url }` and `{ kind: 'stream', stream }`.
**Done looks like:** opening a live Kick channel offers to play it in the same pane a call uses; the picture and the chat are in one window; closing the pane leaves the chat where it was.
**Notes for whoever picks this up:**
- `playback_url` belongs on the existing `kickStream` event, next to the title, category and viewer count. Every frontend that shows video needs it, and the 60-second refresh already running is what will replace an expired token.
- Treat a 403 from a segment as "ask the daemon to refresh this channel", not as a dead stream.
- The renderer's CSP needs `media-src`/`connect-src` for `*.live-video.net`. IVS sends permissive CORS, so no proxy is needed.
- Default the rendition below 1080p60: a 5.8 Mbps 60fps decode beside a chat client is real CPU, and the ladder above gives a cheaper choice.
- Electron's official builds carry H.264/AAC, which is what these renditions are.
Receiving only, as scoped: the daemon puts playbackUrl on the kickStream event, and the renderer plays it with hls.js into the same surface a call uses — now factored out as VideoStage, with the call and the stream as its two sources. Right-click a live channel to watch, the same menu to stop; capped below 1080p60; a 403 on a segment is treated as an expired token and answered with the newest URL the daemon has sent.
Verified live on a streaming channel: 1280x720 playing inline at 759x427, still playing in the corner after navigating away, no Watch entry on an offline channel, and starting one during a call refused.
Finished — https://git.salastil.com/Salastil/nobilis/commit/aad1125 and https://git.salastil.com/Salastil/moho/commit/9e3fa49
Receiving only, as scoped: the daemon puts `playbackUrl` on the `kickStream` event, and the renderer plays it with hls.js into the same surface a call uses — now factored out as `VideoStage`, with the call and the stream as its two sources. Right-click a live channel to watch, the same menu to stop; capped below 1080p60; a 403 on a segment is treated as an expired token and answered with the newest URL the daemon has sent.
Verified live on a streaming channel: 1280x720 playing inline at 759x427, still playing in the corner after navigating away, no Watch entry on an offline channel, and starting one during a call refused.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A Kick channel is a stream as much as a chat, and moho shows only the chat: the header says what is on air, how many people are watching and for how long, but the video itself is somewhere else, in a browser tab beside the window that has the conversation in it.
This is not a player of its own. It is the same video surface a Discord DM video call and a Matrix group call will draw into - one pane, one pop-out, one picture-in-picture, one set of controls - with Kick as a third source feeding it.
Blocked on, and deliberately so: #14 (Discord screen share, hosting and viewing) and #81 (Matrix voice and video calls). Those two settle the hard questions - where real-time media lives, how a call is laid out, what the controls are, whether audio comes through the daemon or the renderer - and Kick is the easy source that should adopt those answers rather than invent its own and be rewritten later. Do not start this one first.
What Kick serves, measured rather than assumed.
GET /api/v2/channels/{slug}already returns aplayback_url, which the daemon parses past today:That is Amazon IVS. It fetches anonymously - 200,
application/vnd.apple.mpegurl, no bearer token, the signed token is already in the query string - and it is a standard HLS multivariant playlist, not LL-HLS. A live channel offered five renditions: 1080p60 at 5.8 Mbps, 720p60 at 3.4, 480p30 at 1.4, 360p30 at 630 kbps and 160p30 at 230 kbps.Why the transport cannot be shared even though the surface can. Kick is one-to-many HLS over a CDN and needs an MSE player - hls.js, or Amazon's own IVS web SDK if the latency matters. Discord and Matrix screenshare are real-time media and arrive as a
MediaStreamattached tosrcObject, which never touches MSE. So the reusable part is everything above the pipe: the surface, the layout, the pop-out, the sizing, and the<video>element itself. The shape that fits is one video surface with two source adapters -{ kind: 'hls', url }and{ kind: 'stream', stream }.Done looks like: opening a live Kick channel offers to play it in the same pane a call uses; the picture and the chat are in one window; closing the pane leaves the chat where it was.
Notes for whoever picks this up:
playback_urlbelongs on the existingkickStreamevent, next to the title, category and viewer count. Every frontend that shows video needs it, and the 60-second refresh already running is what will replace an expired token.media-src/connect-srcfor*.live-video.net. IVS sends permissive CORS, so no proxy is needed.Finished — https://git.salastil.com/Salastil/nobilis/commit/aad1125 and https://git.salastil.com/Salastil/moho/commit/9e3fa49
Receiving only, as scoped: the daemon puts
playbackUrlon thekickStreamevent, and the renderer plays it with hls.js into the same surface a call uses — now factored out asVideoStage, with the call and the stream as its two sources. Right-click a live channel to watch, the same menu to stop; capped below 1080p60; a 403 on a segment is treated as an expired token and answered with the newest URL the daemon has sent.Verified live on a streaming channel: 1280x720 playing inline at 759x427, still playing in the corner after navigating away, no Watch entry on an offline channel, and starting one during a call refused.