Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev/payload.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const buildConfigWithMemoryDB = async () => {
// Return the URL from your custom `externalUrl` field
return ((doc?.stream as { thumbnailUrl?: string })?.thumbnailUrl as string) || null
},
disableLocalStorage: true,
staticDir: path.resolve(dirname, 'media'),
},
},
Expand Down
88 changes: 86 additions & 2 deletions src/adapters/cloudflareStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ type CloudflareStreamAPIResponse = {
messages?: string[]
result: {
created?: string
duration?: number
input?: {
height: number
width: number
}
meta?: {
[key: string]: boolean | null | number | string
}
Expand All @@ -15,6 +20,7 @@ type CloudflareStreamAPIResponse = {
hls?: string
}
readyToStream: boolean
readyToStreamAt?: string
requireSignedURLs?: boolean
size?: number
status?: {
Expand All @@ -39,14 +45,18 @@ const streamResponseFromCloudflareStreamAPI = (
result: response.result
? {
...response.result,
durationInSeconds: response.result.duration,
height: response.result.input?.height,
meta: response.result.meta
? {
name:
typeof response.result.meta.name === 'string' ? response.result.meta.name : '',
...response.result.meta,
}
: undefined,
readyToStreamAt: response.result.readyToStreamAt,
videoId: response.result.uid,
width: response.result.input?.width,
}
: undefined,
success: response.success,
Expand All @@ -58,6 +68,7 @@ class CloudflareStreamAdapter implements StreamAdapter {
private readonly apiToken: string
private readonly baseUrl: string
private readonly customerSubdomain: string
private readonly generateDownloads: { audio?: boolean; video?: boolean } | boolean
private readonly requireSignedURLs: boolean

readonly providerName = 'cloudflare_stream'
Expand All @@ -67,14 +78,55 @@ class CloudflareStreamAdapter implements StreamAdapter {
accountId: string,
customerSubdomain: string,
requireSignedURLs = false,
generateDownloads: { audio?: boolean; video?: boolean } | boolean = true,
Comment thread
webowodev marked this conversation as resolved.
) {
this.apiToken = apiToken
this.accountId = accountId
this.baseUrl = 'https://api.cloudflare.com/client/v4'
this.requireSignedURLs = requireSignedURLs
this.generateDownloads = generateDownloads
this.customerSubdomain = customerSubdomain
}

/**
* Generate download links for a video or audio
*
* @param videoId
*/
private async generateDownloadLinks(videoId: string): Promise<void> {
const url = `${this.baseUrl}/accounts/${this.accountId}/stream/${videoId}/downloads`

const urls = []

if (this.isGenerateAudioDownloadEnabled) {
urls.push(`${url}/audio`)
}

if (this.isGenerateVideoDownloadEnabled) {
urls.push(`${url}`)
}

try {
await Promise.all(
urls.map(async (endpoint) => {
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${this.apiToken}`,
'Content-Type': 'application/json',
},
method: 'POST',
})

if (!response.ok) {
throw new Error(`Failed to generate download link: ${response.statusText}`)
}
}),
)
} catch (error) {
console.error('Error generating download links:', error)
}
}

/**
* Copies a video within Cloudflare Stream.
*
Expand Down Expand Up @@ -207,7 +259,17 @@ class CloudflareStreamAdapter implements StreamAdapter {

const body = await response.json()

return streamResponseFromCloudflareStreamAPI(body)
const result = streamResponseFromCloudflareStreamAPI(body)

// Generate download links if enabled
if (
(this.isGenerateAudioDownloadEnabled || this.isGenerateVideoDownloadEnabled) &&
result.result?.readyToStream
) {
await this.generateDownloadLinks(videoId)
}

return result
}

/**
Expand Down Expand Up @@ -241,18 +303,40 @@ class CloudflareStreamAdapter implements StreamAdapter {

return result
}

private get isGenerateAudioDownloadEnabled(): boolean {
if (typeof this.generateDownloads === 'boolean') {
return this.generateDownloads
}
return this.generateDownloads.audio === true
}

private get isGenerateVideoDownloadEnabled(): boolean {
if (typeof this.generateDownloads === 'boolean') {
return this.generateDownloads
}
return this.generateDownloads.video === true
}
}

export const cloudflareStreamAdapter = ({
accountId,
apiToken,
customerSubdomain,
generateDownloads,
requireSignedURLs,
}: {
accountId: string
apiToken: string
customerSubdomain: string
generateDownloads?: { audio?: boolean; video?: boolean } | boolean
requireSignedURLs?: boolean
}): StreamAdapter => {
return new CloudflareStreamAdapter(apiToken, accountId, customerSubdomain, requireSignedURLs)
return new CloudflareStreamAdapter(
apiToken,
accountId,
customerSubdomain,
requireSignedURLs,
generateDownloads ?? true,
)
}
10 changes: 10 additions & 0 deletions src/adapters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@ export type StreamResponse = {
messages?: string[]
result?: {
created?: string
durationInSeconds?: number
height?: number
meta?: StreamVideoMeta
modified?: string
playback?: {
dash?: string
hls?: string
}
readyToStream: boolean
readyToStreamAt?: string
requireSignedURLs?: boolean
size?: number
status?: {
Expand All @@ -30,15 +33,22 @@ export type StreamResponse = {
}
thumbnail?: string
videoId: string
width?: number
}
success: boolean
}

export type StreamFieldData = {
downloadable?: boolean
durationInSeconds?: number
error: string
height?: number
provider: string
readyToStream: boolean
readyToStreamAt?: string
requireSignedURLs: boolean
size?: number
thumbnailUrl: string
videoId: string
width?: number
}
46 changes: 43 additions & 3 deletions src/fields/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ export const streamField = ({ adapter }: { adapter: StreamAdapter }): Field => {
name: 'videoId',
type: 'text',
admin: {
hidden: true,
readOnly: true,
},
},
{
name: 'thumbnailUrl',
type: 'text',
admin: {
hidden: true,
readOnly: true,
},
},
Expand All @@ -40,10 +42,51 @@ export const streamField = ({ adapter }: { adapter: StreamAdapter }): Field => {
},
defaultValue: false,
},
{
name: 'readyToStreamAt',
type: 'date',
admin: {
hidden: true,
readOnly: true,
},
},
{
name: 'durationInSeconds',
type: 'number',
admin: {
hidden: true,
readOnly: true,
},
},
{
name: 'width',
type: 'number',
admin: {
hidden: true,
readOnly: true,
},
},
{
name: 'height',
type: 'number',
admin: {
hidden: true,
readOnly: true,
},
},
{
name: 'size',
type: 'number',
admin: {
hidden: true,
readOnly: true,
},
},
{
name: 'provider',
type: 'text',
admin: {
hidden: true,
readOnly: true,
},
},
Expand All @@ -61,7 +104,6 @@ export const streamField = ({ adapter }: { adapter: StreamAdapter }): Field => {
admin: {
description:
'If enabled, the video stream URLs will require signed URLs for access, enhancing security.',
// hidden: true,
readOnly: true,
},
defaultValue: false,
Expand All @@ -70,5 +112,3 @@ export const streamField = ({ adapter }: { adapter: StreamAdapter }): Field => {
],
}
}

// generate types from field
15 changes: 10 additions & 5 deletions src/hooks/afterOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,24 @@ export const updateStatusHook = (
result.stream.videoId
) {
// if there is a stream uid but not ready to stream, fetch latest status
const status = await adapter.getStatus(result.stream.videoId)
const response = await adapter.getStatus(result.stream.videoId)

// update video document with latest stream status
await req.payload.update({
id: result.id,
collection: collectionSlug,
data: {
stream: {
error: status.result?.status?.errorReasonText || '',
durationInSeconds: response.result?.durationInSeconds,
error: response.result?.status?.errorReasonText || '',
height: response.result?.height,
provider: adapter.providerName || '',
readyToStream: status.result?.readyToStream,
requireSignedURLs: status.result?.requireSignedURLs || false,
thumbnailUrl: status.result?.thumbnail || '',
readyToStream: response.result?.readyToStream,
readyToStreamAt: response.result?.readyToStreamAt,
requireSignedURLs: response.result?.requireSignedURLs || false,
size: response.result?.size,
thumbnailUrl: response.result?.thumbnail || '',
width: response.result?.width,
},
},
req,
Expand Down