Watts-Lab / deliberation-empirica

Empirica V2 framework
MIT License
6 stars 0 forks source link

Start and end recordings on the server side #684

Closed JamesPHoughton closed 10 months ago

JamesPHoughton commented 11 months ago

According to the docs, the recording is started by one of the participants:

https://docs.daily.co/guides/products/live-streaming-recording/recording-calls-with-the-daily-api#tips-for-the-best-recording-experience

What happens if that person drops out, or reconnects?

JamesPHoughton commented 11 months ago

In tests, if the player who started the recording drops out, it does indeed stop the recording and doesn't restart it.

It looks as though we may be able to start a recording using the API, however, which may not be tied to the individual participant?

https://docs.daily.co/reference/rest-api/rooms/recordings/start#example-requests

JamesPHoughton commented 11 months ago

If we try to start the recording from the onStageStart callback:

Empirica.onStageStart(({ stage }) => {
  const discussion = stage?.get("discussion");
  const { config } = stage.currentGame.batch.get("config");
  const videoStorageLocation = config?.videoStorageLocation;

  if (discussion?.chatType === "video" && videoStorageLocation !== "none") {
    const dailyRoomName = stage.currentGame.get("dailyRoomName");
    startRecording(dailyRoomName);
  }
});

Then we get an error, because nobody has actually joined the room yet.

[server] 21:58:47.230 ERR Error occurred while trying to start recording room 20231214_0258_labDem04XSWF [md [AxiosError]: Request failed with status code 404] {
[server]   code: 'ERR_BAD_REQUEST',
[server]   config: {
[server]     transitional: {
[server]       silentJSONParsing: true,
[server]       forcedJSONParsing: true,
[server]       clarifyTimeoutError: false
[server]     },
[server]     adapter: [Function (anonymous)],
[server]     transformRequest: [ [Function (anonymous)] ],
[server]     transformResponse: [ [Function (anonymous)] ],
[server]     timeout: 0,
[server]     xsrfCookieName: 'XSRF-TOKEN',
[server]     xsrfHeaderName: 'X-XSRF-TOKEN',
[server]     maxContentLength: -1,
[server]     maxBodyLength: -1,
[server]     env: { FormData: [Function] },
[server]     validateStatus: [Function: validateStatus],
[server]     headers: {
[server]       Accept: 'application/json',
[server]       'Content-Type': 'application/json',
[server]       Authorization: 'Bearer 81f1e20093fa32465458e5ca9573695c04a99ee81f0a571057338d4b1cc54896',
[server]       'User-Agent': 'axios/0.27.2',
[server]       'Content-Length': 21
[server]     },
[server]     method: 'post',
[server]     url: 'https://api.daily.co/v1/rooms/20231214_0258_labDem04XSWF/recordings/start',
[server]     data: '{"type":"raw-tracks"}'
[server]   },
[server]   request: <ref *1> ClientRequest {
[server]     _events: [Object: null prototype] {
[server]       abort: [Function (anonymous)],
[server]       aborted: [Function (anonymous)],
[server]       connect: [Function (anonymous)],
[server]       error: [Function (anonymous)],
[server]       socket: [Function (anonymous)],
[server]       timeout: [Function (anonymous)],
[server]       prefinish: [Function: requestOnPrefinish]
[server]     },
[server]     _eventsCount: 7,
[server]     _maxListeners: undefined,
[server]     outputData: [],
[server]     outputSize: 0,
[server]     writable: true,
[server]     destroyed: false,
[server]     _last: true,
[server]     chunkedEncoding: false,
[server]     shouldKeepAlive: false,
[server]     maxRequestsOnConnectionReached: false,
[server]     _defaultKeepAlive: true,
[server]     useChunkedEncodingByDefault: true,
[server]     sendDate: false,
[server]     _removedConnection: false,
[server]     _removedContLen: false,
[server]     _removedTE: false,
[server]     _contentLength: null,
[server]     _hasBody: true,
[server]     _trailer: '',
[server]     finished: true,
[server]     _headerSent: true,
[server]     _closed: false,
[server]     socket: TLSSocket {
[server]       _tlsOptions: [Object],
[server]       _secureEstablished: true,
[server]       _securePending: false,
[server]       _newSessionPending: false,
[server]       _controlReleased: true,
[server]       secureConnecting: false,
[server]       _SNICallback: null,
[server]       servername: 'api.daily.co',
[server]       alpnProtocol: false,
[server]       authorized: true,
[server]       authorizationError: null,
[server]       encrypted: true,
[server]       _events: [Object: null prototype],
[server]       _eventsCount: 10,
[server]       connecting: false,
[server]       _hadError: false,
[server]       _parent: null,
[server]       _host: 'api.daily.co',
[server]       _readableState: [ReadableState],
[server]       _maxListeners: undefined,
[server]       _writableState: [WritableState],
[server]       allowHalfOpen: false,
[server]       _sockname: null,
[server]       _pendingData: null,
[server]       _pendingEncoding: '',
[server]       server: undefined,
[server]       _server: null,
[server]       ssl: [TLSWrap],
[server]       _requestCert: true,
[server]       _rejectUnauthorized: true,
[server]       parser: null,
[server]       _httpMessage: [Circular *1],
[server]       [Symbol(res)]: [TLSWrap],
[server]       [Symbol(verified)]: true,
[server]       [Symbol(pendingSession)]: null,
[server]       [Symbol(async_id_symbol)]: 3386,
[server]       [Symbol(kHandle)]: [TLSWrap],
[server]       [Symbol(lastWriteQueueSize)]: 0,
[server]       [Symbol(timeout)]: null,
[server]       [Symbol(kBuffer)]: null,
[server]       [Symbol(kBufferCb)]: null,
[server]       [Symbol(kBufferGen)]: null,
[server]       [Symbol(kCapture)]: false,
[server]       [Symbol(kSetNoDelay)]: false,
[server]       [Symbol(kSetKeepAlive)]: true,
[server]       [Symbol(kSetKeepAliveInitialDelay)]: 60,
[server]       [Symbol(kBytesRead)]: 0,
[server]       [Symbol(kBytesWritten)]: 0,
[server]       [Symbol(connect-options)]: [Object],
[server]       [Symbol(RequestTimeout)]: undefined
[server]     },
[server]     _header: 'POST /v1/rooms/20231214_0258_labDem04XSWF/recordings/start HTTP/1.1\r\n' +
[server]       'Accept: application/json\r\n' +
[server]       'Content-Type: application/json\r\n' +
[server]       'Authorization: Bearer 81f1e20093fa32465458e5ca9573695c04a99ee81f0a571057338d4b1cc54896\r\n' +
[server]       'User-Agent: axios/0.27.2\r\n' +
[server]       'Content-Length: 21\r\n' +
[server]       'Host: api.daily.co\r\n' +
[server]       'Connection: close\r\n' +
[server]       '\r\n',
[server]     _keepAliveTimeout: 0,
[server]     _onPendingData: [Function: nop],
[server]     agent: Agent {
[server]       _events: [Object: null prototype],
[server]       _eventsCount: 2,
[server]       _maxListeners: undefined,
[server]       defaultPort: 443,
[server]       protocol: 'https:',
[server]       options: [Object: null prototype],
[server]       requests: [Object: null prototype] {},
[server]       sockets: [Object: null prototype],
[server]       freeSockets: [Object: null prototype] {},
[server]       keepAliveMsecs: 1000,
[server]       keepAlive: false,
[server]       maxSockets: Infinity,
[server]       maxFreeSockets: 256,
[server]       scheduling: 'lifo',
[server]       maxTotalSockets: Infinity,
[server]       totalSocketCount: 1,
[server]       maxCachedSessions: 100,
[server]       _sessionCache: [Object],
[server]       [Symbol(kCapture)]: false
[server]     },
[server]     socketPath: undefined,
[server]     method: 'POST',
[server]     maxHeaderSize: undefined,
[server]     insecureHTTPParser: undefined,
[server]     path: '/v1/rooms/20231214_0258_labDem04XSWF/recordings/start',
[server]     _ended: true,
[server]     res: IncomingMessage {
[server]       _readableState: [ReadableState],
[server]       _events: [Object: null prototype],
[server]       _eventsCount: 4,
[server]       _maxListeners: undefined,
[server]       socket: [TLSSocket],
[server]       httpVersionMajor: 1,
[server]       httpVersionMinor: 1,
[server]       httpVersion: '1.1',
[server]       complete: true,
[server]       rawHeaders: [Array],
[server]       rawTrailers: [],
[server]       aborted: false,
[server]       upgrade: false,
[server]       url: '',
[server]       method: null,
[server]       statusCode: 404,
[server]       statusMessage: 'Not Found',
[server]       client: [TLSSocket],
[server]       _consuming: false,
[server]       _dumped: false,
[server]       req: [Circular *1],
[server]       responseUrl: 'https://api.daily.co/v1/rooms/20231214_0258_labDem04XSWF/recordings/start',
[server]       redirects: [],
[server]       [Symbol(kCapture)]: false,
[server]       [Symbol(kHeaders)]: [Object],
[server]       [Symbol(kHeadersCount)]: 18,
[server]       [Symbol(kTrailers)]: null,
[server]       [Symbol(kTrailersCount)]: 0,
[server]       [Symbol(RequestTimeout)]: undefined
[server]     },
[server]     aborted: false,
[server]     timeoutCb: null,
[server]     upgradeOrConnect: false,
[server]     parser: null,
[server]     maxHeadersCount: null,
[server]     reusedSocket: false,
[server]     host: 'api.daily.co',
[server]     protocol: 'https:',
[server]     _redirectable: Writable {
[server]       _writableState: [WritableState],
[server]       _events: [Object: null prototype],
[server]       _eventsCount: 3,
[server]       _maxListeners: undefined,
[server]       _options: [Object],
[server]       _ended: true,
[server]       _ending: true,
[server]       _redirectCount: 0,
[server]       _redirects: [],
[server]       _requestBodyLength: 21,
[server]       _requestBodyBuffers: [],
[server]       _onNativeResponse: [Function (anonymous)],
[server]       _currentRequest: [Circular *1],
[server]       _currentUrl: 'https://api.daily.co/v1/rooms/20231214_0258_labDem04XSWF/recordings/start',
[server]       [Symbol(kCapture)]: false
[server]     },
[server]     [Symbol(kCapture)]: false,
[server]     [Symbol(kNeedDrain)]: false,
[server]     [Symbol(corked)]: 0,
[server]     [Symbol(kOutHeaders)]: [Object: null prototype] {
[server]       accept: [Array],
[server]       'content-type': [Array],
[server]       authorization: [Array],
[server]       'user-agent': [Array],
[server]       'content-length': [Array],
[server]       host: [Array]
[server]     },
[server]     [Symbol(kUniqueHeaders)]: null
[server]   },
[server]   response: {
[server]     status: 404,
[server]     statusText: 'Not Found',
[server]     headers: {
[server]       date: 'Thu, 14 Dec 2023 02:58:47 GMT',
[server]       'content-type': 'application/json; charset=utf-8',
[server]       'content-length': '107',
[server]       connection: 'close',
[server]       'strict-transport-security': 'max-age=31536000 ; includeSubDomains',
[server]       'access-control-allow-origin': '*',
[server]       'access-control-allow-headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Invite-Token, X-DailySessionId, X-DailyJoinToken, X-DailyAboutClient',
[server]       'access-control-allow-methods': 'OPTIONS,HEAD,GET,POST,PUT,PATCH,DELETE',
[server]       etag: 'W/"6b-0Pyq3gBm5flcZ2EBstaj0JqkOT4"'
[server]     },
[server]     config: {
[server]       transitional: [Object],
[server]       adapter: [Function (anonymous)],
[server]       transformRequest: [Array],
[server]       transformResponse: [Array],
[server]       timeout: 0,
[server]       xsrfCookieName: 'XSRF-TOKEN',
[server]       xsrfHeaderName: 'X-XSRF-TOKEN',
[server]       maxContentLength: -1,
[server]       maxBodyLength: -1,
[server]       env: [Object],
[server]       validateStatus: [Function: validateStatus],
[server]       headers: [Object],
[server]       method: 'post',
[server]       url: 'https://api.daily.co/v1/rooms/20231214_0258_labDem04XSWF/recordings/start',
[server]       data: '{"type":"raw-tracks"}'
[server]     },
[server]     request: <ref *1> ClientRequest {
[server]       _events: [Object: null prototype],
[server]       _eventsCount: 7,
[server]       _maxListeners: undefined,
[server]       outputData: [],
[server]       outputSize: 0,
[server]       writable: true,
[server]       destroyed: false,
[server]       _last: true,
[server]       chunkedEncoding: false,
[server]       shouldKeepAlive: false,
[server]       maxRequestsOnConnectionReached: false,
[server]       _defaultKeepAlive: true,
[server]       useChunkedEncodingByDefault: true,
[server]       sendDate: false,
[server]       _removedConnection: false,
[server]       _removedContLen: false,
[server]       _removedTE: false,
[server]       _contentLength: null,
[server]       _hasBody: true,
[server]       _trailer: '',
[server]       finished: true,
[server]       _headerSent: true,
[server]       _closed: false,
[server]       socket: [TLSSocket],
[server]       _header: 'POST /v1/rooms/20231214_0258_labDem04XSWF/recordings/start HTTP/1.1\r\n' +
[server]         'Accept: application/json\r\n' +
[server]         'Content-Type: application/json\r\n' +
[server]         'Authorization: Bearer 81f1e20093fa32465458e5ca9573695c04a99ee81f0a571057338d4b1cc54896\r\n' +
[server]         'User-Agent: axios/0.27.2\r\n' +
[server]         'Content-Length: 21\r\n' +
[server]         'Host: api.daily.co\r\n' +
[server]         'Connection: close\r\n' +
[server]         '\r\n',
[server]       _keepAliveTimeout: 0,
[server]       _onPendingData: [Function: nop],
[server]       agent: [Agent],
[server]       socketPath: undefined,
[server]       method: 'POST',
[server]       maxHeaderSize: undefined,
[server]       insecureHTTPParser: undefined,
[server]       path: '/v1/rooms/20231214_0258_labDem04XSWF/recordings/start',
[server]       _ended: true,
[server]       res: [IncomingMessage],
[server]       aborted: false,
[server]       timeoutCb: null,
[server]       upgradeOrConnect: false,
[server]       parser: null,
[server]       maxHeadersCount: null,
[server]       reusedSocket: false,
[server]       host: 'api.daily.co',
[server]       protocol: 'https:',
[server]       _redirectable: [Writable],
[server]       [Symbol(kCapture)]: false,
[server]       [Symbol(kNeedDrain)]: false,
[server]       [Symbol(corked)]: 0,
[server]       [Symbol(kOutHeaders)]: [Object: null prototype],
[server]       [Symbol(kUniqueHeaders)]: null
[server]     },
[server]     data: {
[server]       error: 'not-found',
[server]       info: 'room 20231214_0258_labDem04XSWF does not seem to be hosting a call currently'
[server]     }
[server]   }
[server] }

We could still use this, and set a 3-second retry for up to 10 retries?

Alternately, when anyone successfully joins the room, have them set a flag on the stage object to say so, and have a callback watching that:

// client/src/components/VideoCall.jsx

...
callFrame.on("joined-meeting", (event) => {
      stage.set("callStarted", true);
    });
...
// server/src/callbacks.js

Empirica.on("stage", "callStarted", async (ctx, { stage, callStarted }) => {
  if (!callStarted) return;
  const discussion = stage?.get("discussion");
  const { config } = stage.currentGame.batch.get("config");
  const videoStorageLocation = config?.videoStorageLocation;

  if (discussion?.chatType === "video" && videoStorageLocation !== "none") {
    const dailyRoomName = stage.currentGame.get("dailyRoomName");
    startRecording(dailyRoomName);
  }
});

We may still have the timing issue, and may have to set a 3-second fallback anyways. Unfortunately, this strategy is currently running afoul of https://github.com/empiricaly/empirica/issues/453

JamesPHoughton commented 10 months ago

When any player joins a call, and we get the "joined-meeting" callback from daily, stage.set("callStarted", true). Then look for this value to be set in a callback, and try to start the recording. Give 10 retries with a 3 second delay.

JamesPHoughton commented 10 months ago

closed in https://github.com/Watts-Lab/deliberation-empirica/pull/690