Skip to content

Commit fc52c3b

Browse files
committed
Return live thumbnail URL from playback API
1 parent 89f7bfa commit fc52c3b

File tree

5 files changed

+68
-66
lines changed

5 files changed

+68
-66
lines changed

packages/api/src/controllers/clip.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ app.post("/", validatePost("clip-payload"), async (req, res) => {
130130
"The provided session id does not belong to this stream"
131131
);
132132
}
133-
({ url, objectStoreId } = await buildRecordingUrl(session, req));
133+
({ url, objectStoreId } = await buildRecordingUrl(
134+
session,
135+
req.config.recordCatalystObjectStoreId,
136+
req.config.secondaryRecordObjectStoreId
137+
));
134138
} else {
135139
({ url, session, objectStoreId } = await getRunningRecording(
136140
content,

packages/api/src/controllers/playback.ts

+16-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ function newPlaybackInfo(
4040
staticFilesPlaybackInfo?: StaticPlaybackInfo[],
4141
live?: PlaybackInfo["meta"]["live"],
4242
recordingUrl?: string,
43-
withRecordings?: boolean
43+
withRecordings?: boolean,
44+
thumbUrl?: string
4445
): PlaybackInfo {
4546
let playbackInfo: PlaybackInfo = {
4647
type,
@@ -89,6 +90,13 @@ function newPlaybackInfo(
8990
});
9091
}
9192
}
93+
if (thumbUrl) {
94+
playbackInfo.meta.source.push({
95+
hrn: "Thumbnail",
96+
type: "image/jpeg",
97+
url: thumbUrl,
98+
});
99+
}
92100

93101
return playbackInfo;
94102
}
@@ -226,10 +234,13 @@ async function getPlaybackInfo(
226234
}
227235

228236
if (stream) {
237+
const thumbsEnabled = await isExperimentSubject("live-thumbs", req.user.id);
229238
let url: string;
230-
if (withRecordings) {
231-
({ url } = await getRunningRecording(stream, req));
239+
let thumbUrl: string;
240+
if (withRecordings || thumbsEnabled) {
241+
({ url, thumbUrl } = await getRunningRecording(stream, req));
232242
}
243+
233244
return newPlaybackInfo(
234245
"live",
235246
getHLSPlaybackUrl(ingest, stream),
@@ -238,7 +249,8 @@ async function getPlaybackInfo(
238249
null,
239250
stream.isActive ? 1 : 0,
240251
url,
241-
withRecordings
252+
withRecordings,
253+
thumbsEnabled && thumbUrl
242254
);
243255
}
244256

packages/api/src/controllers/session.ts

+40-34
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ app.get("/:id", authorizer({}), async (req, res) => {
158158
let originalRecordingUrl: string | null = null;
159159

160160
if (req.query.sourceRecording === "true") {
161-
const { url } = await buildRecordingUrl(session, req);
161+
const { url } = await buildRecordingUrl(
162+
session,
163+
req.config.recordCatalystObjectStoreId,
164+
req.config.secondaryRecordObjectStoreId
165+
);
162166
originalRecordingUrl = url;
163167
session.sourceRecordingUrl = originalRecordingUrl;
164168
}
@@ -230,56 +234,58 @@ export async function getRunningRecording(content: DBStream, req: Request) {
230234
};
231235
}
232236

233-
return await buildRecordingUrl(session, req);
237+
return await buildRecordingUrl(
238+
session,
239+
req.config.recordCatalystObjectStoreId,
240+
req.config.secondaryRecordObjectStoreId
241+
);
234242
}
235243

236-
export async function buildRecordingUrl(session: DBSession, req: Request) {
237-
let objectStoreId: string;
238-
const os = await db.objectStore.get(req.config.recordCatalystObjectStoreId);
244+
export async function buildRecordingUrl(
245+
session: DBSession,
246+
recordCatalystObjectStoreId: string,
247+
secondaryRecordObjectStoreId: string
248+
) {
249+
const os = await db.objectStore.get(recordCatalystObjectStoreId);
239250

240-
let url = pathJoin(
241-
os.publicUrl,
242-
session.playbackId,
243-
session.id,
244-
"output.m3u8"
245-
);
251+
let urlPrefix = pathJoin(os.publicUrl, session.playbackId, session.id);
252+
let manifestUrl = pathJoin(urlPrefix, "output.m3u8");
246253

247254
let params = {
248255
method: "HEAD",
249256
timeout: 5 * 1000,
250257
};
251-
let resp = await fetchWithTimeout(url, params);
258+
let resp = await fetchWithTimeout(manifestUrl, params);
259+
if (resp.status == 200) {
260+
return {
261+
url: manifestUrl,
262+
session,
263+
objectStoreId: recordCatalystObjectStoreId,
264+
thumbUrl: pathJoin(urlPrefix, "source", "latest.jpg"),
265+
};
266+
}
252267

253-
if (resp.status != 200) {
254-
const secondaryOs = req.config.secondaryRecordObjectStoreId
255-
? await db.objectStore.get(req.config.secondaryRecordObjectStoreId)
256-
: undefined;
257-
url = pathJoin(
258-
secondaryOs.publicUrl,
259-
session.playbackId,
260-
session.id,
261-
"output.m3u8"
262-
);
268+
const secondaryOs = await db.objectStore.get(secondaryRecordObjectStoreId);
269+
urlPrefix = pathJoin(secondaryOs.publicUrl, session.playbackId, session.id);
270+
manifestUrl = pathJoin(urlPrefix, "output.m3u8");
263271

264-
objectStoreId = req.config.secondaryRecordObjectStoreId;
272+
const objectStoreId = secondaryRecordObjectStoreId;
265273

266-
resp = await fetchWithTimeout(url, params);
274+
resp = await fetchWithTimeout(manifestUrl, params);
267275

268-
if (resp.status != 200) {
269-
return {
270-
url: null,
271-
session,
272-
objectStoreId,
273-
};
274-
}
275-
} else {
276-
objectStoreId = req.config.recordCatalystObjectStoreId;
276+
if (resp.status != 200) {
277+
return {
278+
url: null,
279+
session,
280+
objectStoreId,
281+
};
277282
}
278283

279284
return {
280-
url,
285+
url: manifestUrl,
281286
session,
282287
objectStoreId,
288+
thumbUrl: pathJoin(urlPrefix, "source", "latest.jpg"),
283289
};
284290
}
285291
export default app;

packages/api/src/schema/api-schema.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -2094,13 +2094,15 @@ components:
20942094
- HLS (TS)
20952095
- MP4
20962096
- WebRTC (H264)
2097+
- Thumbnail
20972098
type:
20982099
type: string
20992100
example: html5/video/mp4
21002101
enum:
21012102
- html5/application/vnd.apple.mpegurl
21022103
- html5/video/mp4
21032104
- html5/video/h264
2105+
- image/jpeg
21042106
url:
21052107
type: string
21062108
example: >-

packages/api/src/webhooks/cannon.ts

+5-27
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { DBStream } from "../store/stream-table";
1919
import { USER_SESSION_TIMEOUT } from "../controllers/stream";
2020
import { BadRequestError, UnprocessableEntityError } from "../store/errors";
2121
import { db } from "../store";
22+
import { buildRecordingUrl } from "../controllers/session";
2223

2324
const WEBHOOK_TIMEOUT = 5 * 1000;
2425
const MAX_BACKOFF = 60 * 60 * 1000;
@@ -562,35 +563,12 @@ export default class WebhookCannon {
562563
this.queue
563564
);
564565

565-
const os = await db.objectStore.get(this.recordCatalystObjectStoreId);
566-
// we can't rate limit this task because it's not a user action
567-
let url = pathJoin(
568-
os.publicUrl,
569-
session.playbackId,
570-
session.id,
571-
"output.m3u8"
566+
const { url } = await buildRecordingUrl(
567+
session,
568+
this.recordCatalystObjectStoreId,
569+
this.secondaryRecordObjectStoreId
572570
);
573571

574-
const secondaryOs = this.secondaryRecordObjectStoreId
575-
? await db.objectStore.get(this.secondaryRecordObjectStoreId)
576-
: undefined;
577-
if (secondaryOs) {
578-
let params = {
579-
method: "HEAD",
580-
timeout: 5 * 1000,
581-
};
582-
const resp = await fetchWithTimeout(url, params);
583-
584-
if (resp.status != 200) {
585-
url = pathJoin(
586-
secondaryOs.publicUrl,
587-
session.playbackId,
588-
session.id,
589-
"output.m3u8"
590-
);
591-
}
592-
}
593-
594572
await taskScheduler.createAndScheduleTask(
595573
"upload",
596574
{

0 commit comments

Comments
 (0)