Skip to content
This repository has been archived by the owner on Dec 24, 2024. It is now read-only.

feat(multistream): Experimental multistream #25

Merged
merged 9 commits into from
Nov 10, 2024

Conversation

Entrivax
Copy link
Contributor

This PR goes in pair with Zibbp/ganymede#355!

Adds a multistream page for playlists.
It uses playlist as a way to group multiple streams together, then allows to play them together, in sync (granted the Streamed at date is well filled.)
Accessible from the menu in the playlist page.

I had to implement another version of the video player, to allow multiple ones to be sync together. It will probably need a big refactoring between the two players (VideoPlayer and SyncedVideoPlayer) to merge them, since it would need to rework the current VOD page, and probably the chat player too.

In the multistream page, there is no chat for the moment, it will come later, after trying to refactor both the ChatPlayer and the ExperimentalChatPlayer

@Entrivax Entrivax marked this pull request as ready for review January 13, 2024 20:07
@Entrivax Entrivax force-pushed the experiment/multistream branch from ba80631 to 4aa2363 Compare November 10, 2024 01:19
@Entrivax
Copy link
Contributor Author

After a lot of time, I rebased this PR and done a few changes to the UI, here is the current version:
https://www.youtube.com/watch?v=aKQGbw2gb70

Since last time, the synchronization is now more stable, but at the cost of being able to play/pause and seek from the different players, that's why the seek and the play/pause buttons were added to the drawer containing the timeline.

@Zibbp
Copy link
Owner

Zibbp commented Nov 10, 2024

Wow this looks really good, thank you for these two PRs!
I have a couple of questions

  1. You're using vod.streamed_at to determine the start times for videos. How about adding a toggle to select between streamed_at and created_at. When testing this I ran into what I believe is an issue if a user starts archiving a livestream mid-stream. The timelines are way off because one streamer started a lot earlier and because I stopped the archives early (the videos are only two minutes long). I believe a toggle to use created_at would help here as all the videos were created at roughly the same time. What do you think? Or maybe I'm misusing this feature, and it requires VODs?
    firefox_4MmirWpWjm

  2. What do you think about moving the video controls to a box at the top or bottom of the screen that is persisted so you don't have to open the drawer to play/pause?
    image

I'm in the early stages of refactoring a lot of the frontend code to be more type-safe and clean. When that is more complete I can take a look at the ability to control the video state (play, pause, seek, etc) using the players instead of static buttons. I believe it should be possible as the player API can emit states. No need to implement in this PR as it's probably not easily doable right now.

For the release notes, do you mind if I link your Youtube video demoing the feature?

@Entrivax
Copy link
Contributor Author

Entrivax commented Nov 10, 2024

No problem!

  1. Yep, I didn't thought about livestreams! I could use the type and if it's "archive", use the streamed_at and if it's "live" use the created_at field. I'm fixing it!
  2. I would have done that after reading your feedback if I didn't placed the invisible (to avoid obstruction of the video) close button at the top right of each tile, if you have any idea how I should handle the close in any other way, I'm open!

Oh that's pretty nice! At the beginning, I tried to listen to the play/pause/seek events of the player, except it always send events without distinction if it's either a script or the user that did the action, so infinite looping events, it was really hacky to try to make it work and not really readable, so I tossed it for now.

And sure, no problem!

Let me know if you find another issue!

@Entrivax
Copy link
Contributor Author

Weird, I get an error with the build docker image on the channel page, but not with the dev server:

2024-11-10 22:54:21 AxiosError: connect ECONNREFUSED 127.0.0.1:4800
2024-11-10 22:54:21     at AxiosError.from (file:///app/node_modules/axios/lib/core/AxiosError.js:92:14)
2024-11-10 22:54:21     at RedirectableRequest.handleRequestError (file:///app/node_modules/axios/lib/adapters/http.js:620:25)
2024-11-10 22:54:21     at RedirectableRequest.emit (node:events:517:28)
2024-11-10 22:54:21     at eventHandlers.<computed> (/app/node_modules/follow-redirects/index.js:38:24)
2024-11-10 22:54:21     at ClientRequest.emit (node:events:517:28)
2024-11-10 22:54:21     at Socket.socketErrorListener (node:_http_client:501:9)
2024-11-10 22:54:21     at Socket.emit (node:events:517:28)
2024-11-10 22:54:21     at emitErrorNT (node:internal/streams/destroy:151:8)
2024-11-10 22:54:21     at emitErrorCloseNT (node:internal/streams/destroy:116:3)
2024-11-10 22:54:21     at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
2024-11-10 22:54:21   port: 4800,
2024-11-10 22:54:21   address: '127.0.0.1',
2024-11-10 22:54:21   syscall: 'connect',
2024-11-10 22:54:21   code: 'ECONNREFUSED',
2024-11-10 22:54:21   errno: -111,
2024-11-10 22:54:21   config: {
2024-11-10 22:53:53  ⚠ "next start" does not work with "output: standalone" configuration. Use "node .next/standalone/server.js" instead.
2024-11-10 22:54:21     transitional: {
2024-11-10 22:54:21       silentJSONParsing: true,
2024-11-10 22:54:21       forcedJSONParsing: true,
2024-11-10 22:54:21       clarifyTimeoutError: false
2024-11-10 22:54:21     },
2024-11-10 22:54:21     adapter: [ 'xhr', 'http', 'fetch' ],
2024-11-10 22:54:21     transformRequest: [ [Function: transformRequest] ],
2024-11-10 22:54:21     transformResponse: [ [Function: transformResponse] ],
2024-11-10 22:54:21     timeout: 0,
2024-11-10 22:54:21     xsrfCookieName: 'XSRF-TOKEN',
2024-11-10 22:54:21     xsrfHeaderName: 'X-XSRF-TOKEN',
2024-11-10 22:54:21     maxContentLength: -1,
2024-11-10 22:54:21     maxBodyLength: -1,
2024-11-10 22:54:21     env: { FormData: [Function], Blob: [class Blob] },
2024-11-10 22:54:21     validateStatus: [Function: validateStatus],
2024-11-10 22:54:21     headers: Object [AxiosHeaders] {
2024-11-10 22:54:21       Accept: 'application/json, text/plain, */*',
2024-11-10 22:54:21       'Content-Type': 'application/json',
2024-11-10 22:54:21       'User-Agent': 'axios/1.7.7',
2024-11-10 22:54:21       'Accept-Encoding': 'gzip, compress, deflate, br'
2024-11-10 22:54:21     },
2024-11-10 22:54:21     baseURL: 'http://127.0.0.1:4800',
2024-11-10 22:54:21     method: 'get',
2024-11-10 22:54:21     url: '/api/v1/channel/name/shylily',
2024-11-10 22:54:21     data: undefined
2024-11-10 22:54:21   },
2024-11-10 22:54:21   request: <ref *1> Writable {
2024-11-10 22:54:21     _writableState: WritableState {
2024-11-10 22:54:21       objectMode: false,
2024-11-10 22:54:21       highWaterMark: 16384,
2024-11-10 22:54:21       finalCalled: false,
2024-11-10 22:54:21       needDrain: false,
2024-11-10 22:54:21       ending: false,
2024-11-10 22:54:21       ended: false,
2024-11-10 22:54:21       finished: false,
2024-11-10 22:54:21       destroyed: false,
2024-11-10 22:54:21       decodeStrings: true,
2024-11-10 22:54:21       defaultEncoding: 'utf8',
2024-11-10 22:54:21       length: 0,
2024-11-10 22:54:21       writing: false,
2024-11-10 22:54:21       corked: 0,
2024-11-10 22:54:21       sync: true,
2024-11-10 22:54:21       bufferProcessing: false,
2024-11-10 22:54:21       onwrite: [Function: bound onwrite],
2024-11-10 22:54:21       writecb: null,
2024-11-10 22:54:21       writelen: 0,
2024-11-10 22:54:21       afterWriteTickInfo: null,
2024-11-10 22:54:21       buffered: [],
2024-11-10 22:54:21       bufferedIndex: 0,
2024-11-10 22:54:21       allBuffers: true,
2024-11-10 22:54:21       allNoop: true,
2024-11-10 22:54:21       pendingcb: 0,
2024-11-10 22:54:21       constructed: true,
2024-11-10 22:54:21       prefinished: false,
2024-11-10 22:54:21       errorEmitted: false,
2024-11-10 22:54:21       emitClose: true,
2024-11-10 22:54:21       autoDestroy: true,
2024-11-10 22:54:21       errored: null,
2024-11-10 22:54:21       closed: false,
2024-11-10 22:54:21       closeEmitted: false,
2024-11-10 22:54:21       [Symbol(kOnFinished)]: []
2024-11-10 22:54:21     },
2024-11-10 22:54:21     _events: [Object: null prototype] {
2024-11-10 22:54:21       response: [Function: handleResponse],
2024-11-10 22:54:21       error: [Function: handleRequestError],
2024-11-10 22:54:21       socket: [Function: handleRequestSocket]
2024-11-10 22:54:21     },
2024-11-10 22:54:21     _eventsCount: 3,
2024-11-10 22:54:21     _maxListeners: undefined,
2024-11-10 22:54:21     _options: {
2024-11-10 22:54:21       maxRedirects: 21,
2024-11-10 22:54:21       maxBodyLength: Infinity,
2024-11-10 22:54:21       protocol: 'http:',
2024-11-10 22:54:21       path: '/api/v1/channel/name/shylily',
2024-11-10 22:54:21       method: 'GET',
2024-11-10 22:54:21       headers: [Object: null prototype],
2024-11-10 22:54:21       agents: [Object],
2024-11-10 22:54:21       auth: undefined,
2024-11-10 22:54:21       family: undefined,
2024-11-10 22:54:21       beforeRedirect: [Function: dispatchBeforeRedirect],
2024-11-10 22:54:21       beforeRedirects: [Object],
2024-11-10 22:54:21       hostname: '127.0.0.1',
2024-11-10 22:54:21       port: '4800',
2024-11-10 22:54:21       agent: undefined,
2024-11-10 22:54:21       nativeProtocols: [Object],
2024-11-10 22:54:21       pathname: '/api/v1/channel/name/shylily'
2024-11-10 22:54:21     },
2024-11-10 22:54:21     _ended: true,
2024-11-10 22:54:21     _ending: true,
2024-11-10 22:54:21     _redirectCount: 0,
2024-11-10 22:54:21     _redirects: [],
2024-11-10 22:54:21     _requestBodyLength: 0,
2024-11-10 22:54:21     _requestBodyBuffers: [],
2024-11-10 22:54:21     _onNativeResponse: [Function (anonymous)],
2024-11-10 22:54:21     _currentRequest: ClientRequest {
2024-11-10 22:54:21       _events: [Object: null prototype],
2024-11-10 22:54:21       _eventsCount: 7,
2024-11-10 22:54:21       _maxListeners: undefined,
2024-11-10 22:54:21       outputData: [],
2024-11-10 22:54:21       outputSize: 0,
2024-11-10 22:54:21       writable: true,
2024-11-10 22:54:21       destroyed: false,
2024-11-10 22:54:21       _last: true,
2024-11-10 22:54:21       chunkedEncoding: false,
2024-11-10 22:54:21       shouldKeepAlive: false,
2024-11-10 22:54:21       maxRequestsOnConnectionReached: false,
2024-11-10 22:54:21       _defaultKeepAlive: true,
2024-11-10 22:54:21       useChunkedEncodingByDefault: false,
2024-11-10 22:54:21       sendDate: false,
2024-11-10 22:54:21       _removedConnection: false,
2024-11-10 22:54:21       _removedContLen: false,
2024-11-10 22:54:21       _removedTE: false,
2024-11-10 22:54:21       strictContentLength: false,
2024-11-10 22:54:21       _contentLength: 0,
2024-11-10 22:54:21       _hasBody: true,
2024-11-10 22:54:21       _trailer: '',
2024-11-10 22:54:21       finished: true,
2024-11-10 22:54:21       _headerSent: true,
2024-11-10 22:54:21       _closed: false,
2024-11-10 22:54:21       socket: [Socket],
2024-11-10 22:54:21       _header: 'GET /api/v1/channel/name/shylily HTTP/1.1\r\n' +
2024-11-10 22:54:21         'Accept: application/json, text/plain, */*\r\n' +
2024-11-10 22:54:21         'Content-Type: application/json\r\n' +
2024-11-10 22:54:21         'User-Agent: axios/1.7.7\r\n' +
2024-11-10 22:54:21         'Accept-Encoding: gzip, compress, deflate, br\r\n' +
2024-11-10 22:54:21         'Host: 127.0.0.1:4800\r\n' +
2024-11-10 22:54:21         'Connection: close\r\n' +
2024-11-10 22:54:21         '\r\n',
2024-11-10 22:54:21       _keepAliveTimeout: 0,
2024-11-10 22:54:21       _onPendingData: [Function: nop],
2024-11-10 22:54:21       agent: [Agent],
2024-11-10 22:54:21       socketPath: undefined,
2024-11-10 22:54:21       method: 'GET',
2024-11-10 22:54:21       maxHeaderSize: undefined,
2024-11-10 22:54:21       insecureHTTPParser: undefined,
2024-11-10 22:54:21       joinDuplicateHeaders: undefined,
2024-11-10 22:54:21       path: '/api/v1/channel/name/shylily',
2024-11-10 22:54:21       _ended: false,
2024-11-10 22:54:21       res: null,
2024-11-10 22:54:21       aborted: false,
2024-11-10 22:54:21       timeoutCb: null,
2024-11-10 22:54:21       upgradeOrConnect: false,
2024-11-10 22:54:21       parser: null,
2024-11-10 22:54:21       maxHeadersCount: null,
2024-11-10 22:54:21       reusedSocket: false,
2024-11-10 22:54:21       host: '127.0.0.1',
2024-11-10 22:54:21       protocol: 'http:',
2024-11-10 22:54:21       _redirectable: [Circular *1],
2024-11-10 22:54:21       [Symbol(kCapture)]: false,
2024-11-10 22:54:21       [Symbol(kBytesWritten)]: 0,
2024-11-10 22:54:21       [Symbol(kNeedDrain)]: false,
2024-11-10 22:54:21       [Symbol(corked)]: 0,
2024-11-10 22:54:21       [Symbol(kOutHeaders)]: [Object: null prototype],
2024-11-10 22:54:21       [Symbol(errored)]: null,
2024-11-10 22:54:21       [Symbol(kHighWaterMark)]: 16384,
2024-11-10 22:54:21       [Symbol(kRejectNonStandardBodyWrites)]: false,
2024-11-10 22:54:21       [Symbol(kUniqueHeaders)]: null
2024-11-10 22:54:21     },
2024-11-10 22:54:21     _currentUrl: 'http://127.0.0.1:4800/api/v1/channel/name/shylily',
2024-11-10 22:54:21     [Symbol(kCapture)]: false
2024-11-10 22:54:21   },
2024-11-10 22:54:21 HTTP API error TypeError: Cannot read properties of undefined (reading 'status')
2024-11-10 22:54:21     at /app/.next/server/chunks/578.js:1:12570
2024-11-10 22:54:21     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024-11-10 22:54:21     at async Axios.request (file:///app/node_modules/axios/lib/core/Axios.js:40:14)
2024-11-10 22:54:21     at async c (/app/.next/server/chunks/578.js:1:12992)
2024-11-10 22:54:21     at async j (/app/.next/server/pages/channels/[channelName].js:1:6580)
2024-11-10 22:54:21     at async e3 (/app/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js:31:594)
2024-11-10 22:54:21     at async doRender (/app/node_modules/next/dist/server/base-server.js:1405:30)
2024-11-10 22:54:21     at async cacheEntry.responseCache.get.routeKind (/app/node_modules/next/dist/server/base-server.js:1579:28)
2024-11-10 22:54:21     at async NextNodeServer.renderToResponseWithComponentsImpl (/app/node_modules/next/dist/server/base-server.js:1487:28)
2024-11-10 22:54:21     at async NextNodeServer.renderPageComponent (/app/node_modules/next/dist/server/base-server.js:1911:24)
2024-11-10 22:54:21     at Axios.request (file:///app/node_modules/axios/lib/core/Axios.js:45:41)
2024-11-10 22:54:21     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024-11-10 22:54:21     at async c (/app/.next/server/chunks/578.js:1:12992)
2024-11-10 22:54:21     at async j (/app/.next/server/pages/channels/[channelName].js:1:6580)
2024-11-10 22:54:21     at async e3 (/app/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js:31:594)
2024-11-10 22:54:21     at async doRender (/app/node_modules/next/dist/server/base-server.js:1405:30)
2024-11-10 22:54:21     at async cacheEntry.responseCache.get.routeKind (/app/node_modules/next/dist/server/base-server.js:1579:28)
2024-11-10 22:54:21     at async NextNodeServer.renderToResponseWithComponentsImpl (/app/node_modules/next/dist/server/base-server.js:1487:28)
2024-11-10 22:54:21     at async NextNodeServer.renderPageComponent (/app/node_modules/next/dist/server/base-server.js:1911:24)
2024-11-10 22:54:21     at async NextNodeServer.renderToResponseImpl (/app/node_modules/next/dist/server/base-server.js:1949:32)
2024-11-10 22:54:21   cause: Error: connect ECONNREFUSED 127.0.0.1:4800
2024-11-10 22:54:21       at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1555:16) {
2024-11-10 22:54:21     errno: -111,
2024-11-10 22:54:21     code: 'ECONNREFUSED',
2024-11-10 22:54:21     syscall: 'connect',
2024-11-10 22:54:21     address: '127.0.0.1',
2024-11-10 22:54:21     port: 4800
2024-11-10 22:54:21   }
2024-11-10 22:54:21 }
2024-11-10 22:54:21 TypeError: Cannot read properties of undefined (reading 'data')
2024-11-10 22:54:21     at c (/app/.next/server/chunks/578.js:1:13152)
2024-11-10 22:54:21     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
2024-11-10 22:54:21     at async j (/app/.next/server/pages/channels/[channelName].js:1:6580)
2024-11-10 22:54:21     at async e3 (/app/node_modules/next/dist/compiled/next-server/pages.runtime.prod.js:31:594)
2024-11-10 22:54:21     at async doRender (/app/node_modules/next/dist/server/base-server.js:1405:30)
2024-11-10 22:54:21     at async cacheEntry.responseCache.get.routeKind (/app/node_modules/next/dist/server/base-server.js:1579:28)
2024-11-10 22:54:21     at async NextNodeServer.renderToResponseWithComponentsImpl (/app/node_modules/next/dist/server/base-server.js:1487:28)
2024-11-10 22:54:21     at async NextNodeServer.renderPageComponent (/app/node_modules/next/dist/server/base-server.js:1911:24)
2024-11-10 22:54:21     at async NextNodeServer.renderToResponseImpl (/app/node_modules/next/dist/server/base-server.js:1949:32)
2024-11-10 22:54:21     at async NextNodeServer.pipeImpl (/app/node_modules/next/dist/server/base-server.js:916:25)

@Entrivax
Copy link
Contributor Author

Ok, that's not an issue brought by this PR, I have the same thing when building the docker image on the main branch

@Zibbp
Copy link
Owner

Zibbp commented Nov 10, 2024

Yep, I didn't thought about livestreams! I could use the type and if it's "archive", use the streamed_at and if it's "live" use the created_at field. I'm fixing it!

Works great for livestreams now.

I would have done that after reading your feedback if I didn't placed the invisible (to avoid obstruction of the video) close button at the top right of each tile, if you have any idea how I should handle the close in any other way, I'm open!

Maybe expand the button to a small div to support the playback buttons + the expand button?
image

That gets in the way of resizing/moving the videos though, so still an issue. I'm fine with leaving like this until the player itself can handle the video state unless you have any other ideas.

Another question, is it possible to place the streams in a default configuration rather than requiring the user to place the stream every time they open the page? It doesn't have to be too fancy, I'd say one column per video and if the user wants to change it let them.

Regarding the error, it looks like it's trying to statically build in a channel for some reason. It shouldn't be doing that. I'm guessing it's something with your local setup? Maybe try building with --no-cache?

@Entrivax
Copy link
Contributor Author

Entrivax commented Nov 10, 2024

Maybe expand the button to a small div to support the playback buttons + the expand button? image

That gets in the way of resizing/moving the videos though, so still an issue. I'm fine with leaving like this until the player itself can handle the video state unless you have any other ideas.

Yeah, unfortunately, it will get in the way, I will try to find a solution for that and maybe suggest it in an another PR, if that's good for you?

Another question, is it possible to place the streams in a default configuration rather than requiring the user to place the stream every time they open the page? It doesn't have to be too fancy, I'd say one column per video and if the user wants to change it let them.

Done, now it should display every stream side by side by default, and if there is more than 3 streamers, it will display them on 2 rows by default.

Regarding the error, it looks like it's trying to statically build in a channel for some reason. It shouldn't be doing that. I'm guessing it's something with your local setup? Maybe try building with --no-cache?

Yeah, my local env is a Frankenstein's creature right now, maybe there is something weird happening.

@Zibbp
Copy link
Owner

Zibbp commented Nov 10, 2024

Yeah, unfortunately, it will get in the way, I will try to find a solution for that and maybe suggest it in an another PR, if that's good for you?

Works for me. I'll try incorporating this into the player when the frontend refactor is more complete.

Done, now it should display every stream side by side by default, and if there is more than 3 streamers, it will display them on 2 rows by default.

Thanks, this provides a much better user experience.

Copy link
Owner

@Zibbp Zibbp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you merge the two PRs tonight I'll put out a release tonight or tomorrow.

@Entrivax
Copy link
Contributor Author

If you merge the two PRs tonight I'll put out a release tonight or tomorrow.

It's good for me, only you can merge the PR, so I leave it to you!

@Zibbp Zibbp merged commit c1ec0b1 into Zibbp:main Nov 10, 2024
1 check passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants