Skip to content

Commit dbcc079

Browse files
authored
Return live thumbnail URL from playback API (#1953)
* Return live thumbnail URL from playback API (#1938) * Add an undefined check
1 parent 727fdbd commit dbcc079

File tree

7 files changed

+73
-69
lines changed

7 files changed

+73
-69
lines changed

packages/api/src/controllers/asset.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export async function defaultObjectStoreId(
9090

9191
const secondaryStorageEnabled = !(await isExperimentSubject(
9292
primaryStorageExperiment,
93-
user.id
93+
user?.id
9494
));
9595

9696
if (isPrivatePlaybackPolicy(body.playbackPolicy)) {
@@ -817,7 +817,7 @@ const uploadWithUrlHandler: RequestHandler = async (req, res) => {
817817
c2pa,
818818
catalystPipelineStrategy: catalystPipelineStrategy(req),
819819
encryption,
820-
thumbnails: await isExperimentSubject("vod-thumbs", req.user.id),
820+
thumbnails: await isExperimentSubject("vod-thumbs", req.user?.id),
821821
},
822822
},
823823
undefined,

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

+19-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,16 @@ async function getPlaybackInfo(
226234
}
227235

228236
if (stream) {
237+
const thumbsEnabled = await isExperimentSubject(
238+
"live-thumbs",
239+
req?.user?.id
240+
);
229241
let url: string;
230-
if (withRecordings) {
231-
({ url } = await getRunningRecording(stream, req));
242+
let thumbUrl: string;
243+
if (withRecordings || thumbsEnabled) {
244+
({ url, thumbUrl } = await getRunningRecording(stream, req));
232245
}
246+
233247
return newPlaybackInfo(
234248
"live",
235249
getHLSPlaybackUrl(ingest, stream),
@@ -238,7 +252,8 @@ async function getPlaybackInfo(
238252
null,
239253
stream.isActive ? 1 : 0,
240254
url,
241-
withRecordings
255+
withRecordings,
256+
thumbsEnabled && thumbUrl
242257
);
243258
}
244259

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
@@ -2110,13 +2110,15 @@ components:
21102110
- HLS (TS)
21112111
- MP4
21122112
- WebRTC (H264)
2113+
- Thumbnail
21132114
type:
21142115
type: string
21152116
example: html5/video/mp4
21162117
enum:
21172118
- html5/application/vnd.apple.mpegurl
21182119
- html5/video/mp4
21192120
- html5/video/h264
2121+
- image/jpeg
21202122
url:
21212123
type: string
21222124
example: >-

packages/api/src/store/experiment-table.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ForbiddenError, NotFoundError } from "./errors";
66
import Table from "./table";
77
import { WithID } from "./types";
88

9-
export async function isExperimentSubject(experiment: string, userId: string) {
9+
export async function isExperimentSubject(experiment: string, userId?: string) {
1010
try {
1111
const { audienceUserIds, audienceAllowAll } =
1212
await db.experiment.getByNameOrId(experiment);

packages/api/src/webhooks/cannon.ts

+4-27
Original file line numberDiff line numberDiff line change
@@ -575,35 +575,12 @@ export default class WebhookCannon {
575575
this.queue
576576
);
577577

578-
const os = await db.objectStore.get(this.recordCatalystObjectStoreId);
579-
// we can't rate limit this task because it's not a user action
580-
let url = pathJoin(
581-
os.publicUrl,
582-
session.playbackId,
583-
session.id,
584-
"output.m3u8"
578+
const { url } = await buildRecordingUrl(
579+
session,
580+
this.recordCatalystObjectStoreId,
581+
this.secondaryRecordObjectStoreId
585582
);
586583

587-
const secondaryOs = this.secondaryRecordObjectStoreId
588-
? await db.objectStore.get(this.secondaryRecordObjectStoreId)
589-
: undefined;
590-
if (secondaryOs) {
591-
let params = {
592-
method: "HEAD",
593-
timeout: 5 * 1000,
594-
};
595-
const resp = await fetchWithTimeout(url, params);
596-
597-
if (resp.status != 200) {
598-
url = pathJoin(
599-
secondaryOs.publicUrl,
600-
session.playbackId,
601-
session.id,
602-
"output.m3u8"
603-
);
604-
}
605-
}
606-
607584
await taskScheduler.createAndScheduleTask(
608585
"upload",
609586
{

0 commit comments

Comments
 (0)