|
|
@@ -0,0 +1,347 @@
|
|
|
+/**
|
|
|
+ * Cloudflare Stream API 封装
|
|
|
+ *
|
|
|
+ * 使用方式:
|
|
|
+ * 1. 直接调用 Cloudflare API(需要 API Token,适合后端代理)
|
|
|
+ * 2. 通过后端代理调用(推荐,避免暴露 Token)
|
|
|
+ *
|
|
|
+ * API 文档:https://developers.cloudflare.com/stream/
|
|
|
+ */
|
|
|
+
|
|
|
+import axios, { type AxiosInstance } from 'axios'
|
|
|
+import type {
|
|
|
+ CloudflareVideo,
|
|
|
+ CloudflareLiveInput,
|
|
|
+ CloudflareLiveOutput,
|
|
|
+ CloudflareApiResponse,
|
|
|
+ CloudflareListResponse,
|
|
|
+ CreateLiveInputParams,
|
|
|
+ CreateUploadUrlParams,
|
|
|
+ DirectUploadResponse,
|
|
|
+ VideoPlaybackInfo
|
|
|
+} from '@/types/cloudflare'
|
|
|
+
|
|
|
+// Cloudflare Stream 配置
|
|
|
+export interface CloudflareStreamConfig {
|
|
|
+ accountId: string // Cloudflare Account ID
|
|
|
+ apiToken?: string // API Token(直接调用时需要)
|
|
|
+ customerSubdomain?: string // 客户子域名,如 customer-xxx
|
|
|
+ baseUrl?: string // 自定义 API 基础 URL(用于后端代理)
|
|
|
+}
|
|
|
+
|
|
|
+class CloudflareStreamApi {
|
|
|
+ private client: AxiosInstance
|
|
|
+ private config: CloudflareStreamConfig
|
|
|
+
|
|
|
+ constructor(config: CloudflareStreamConfig) {
|
|
|
+ this.config = config
|
|
|
+
|
|
|
+ // 创建 axios 实例
|
|
|
+ this.client = axios.create({
|
|
|
+ baseURL: config.baseUrl || `https://api.cloudflare.com/client/v4/accounts/${config.accountId}/stream`,
|
|
|
+ timeout: 30000,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ ...(config.apiToken ? { 'Authorization': `Bearer ${config.apiToken}` } : {})
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // 响应拦截器
|
|
|
+ this.client.interceptors.response.use(
|
|
|
+ (response) => response.data,
|
|
|
+ (error) => {
|
|
|
+ console.error('Cloudflare Stream API Error:', error.response?.data || error.message)
|
|
|
+ return Promise.reject(error)
|
|
|
+ }
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取客户播放域名
|
|
|
+ */
|
|
|
+ getCustomerDomain(): string {
|
|
|
+ return this.config.customerSubdomain
|
|
|
+ ? `customer-${this.config.customerSubdomain}.cloudflarestream.com`
|
|
|
+ : `customer-${this.config.accountId}.cloudflarestream.com`
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 视频管理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取视频列表
|
|
|
+ */
|
|
|
+ async listVideos(params?: {
|
|
|
+ after?: string
|
|
|
+ before?: string
|
|
|
+ include_counts?: boolean
|
|
|
+ search?: string
|
|
|
+ limit?: number
|
|
|
+ asc?: boolean
|
|
|
+ status?: string
|
|
|
+ }): Promise<CloudflareListResponse<CloudflareVideo>> {
|
|
|
+ return this.client.get('', { params })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取视频详情
|
|
|
+ */
|
|
|
+ async getVideo(videoId: string): Promise<CloudflareApiResponse<CloudflareVideo>> {
|
|
|
+ return this.client.get(`/${videoId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除视频
|
|
|
+ */
|
|
|
+ async deleteVideo(videoId: string): Promise<CloudflareApiResponse<null>> {
|
|
|
+ return this.client.delete(`/${videoId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新视频元数据
|
|
|
+ */
|
|
|
+ async updateVideo(videoId: string, data: {
|
|
|
+ meta?: Record<string, any>
|
|
|
+ allowedOrigins?: string[]
|
|
|
+ requireSignedURLs?: boolean
|
|
|
+ scheduledDeletion?: string
|
|
|
+ uploadExpiry?: string
|
|
|
+ }): Promise<CloudflareApiResponse<CloudflareVideo>> {
|
|
|
+ return this.client.post(`/${videoId}`, data)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 URL 复制视频
|
|
|
+ */
|
|
|
+ async copyFromUrl(url: string, meta?: Record<string, any>): Promise<CloudflareApiResponse<CloudflareVideo>> {
|
|
|
+ return this.client.post('/copy', { url, meta })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 裁剪视频
|
|
|
+ */
|
|
|
+ async clipVideo(videoId: string, params: {
|
|
|
+ clippedFromVideoUID: string
|
|
|
+ startTimeSeconds: number
|
|
|
+ endTimeSeconds: number
|
|
|
+ meta?: Record<string, any>
|
|
|
+ }): Promise<CloudflareApiResponse<CloudflareVideo>> {
|
|
|
+ return this.client.post('/clip', params)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 上传管理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建直接上传 URL(TUS 协议)
|
|
|
+ */
|
|
|
+ async createDirectUploadUrl(params?: CreateUploadUrlParams): Promise<CloudflareApiResponse<DirectUploadResponse['result']>> {
|
|
|
+ return this.client.post('/direct_upload', params || {})
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用 TUS 协议上传视频
|
|
|
+ * 注意:大文件上传建议使用 tus-js-client 库
|
|
|
+ */
|
|
|
+ async uploadWithTus(file: File, uploadUrl: string, onProgress?: (progress: number) => void): Promise<void> {
|
|
|
+ // 动态导入 tus-js-client
|
|
|
+ const { Upload } = await import('tus-js-client')
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const upload = new Upload(file, {
|
|
|
+ endpoint: uploadUrl,
|
|
|
+ retryDelays: [0, 3000, 5000, 10000, 20000],
|
|
|
+ metadata: {
|
|
|
+ filename: file.name,
|
|
|
+ filetype: file.type
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ console.error('Upload failed:', error)
|
|
|
+ reject(error)
|
|
|
+ },
|
|
|
+ onProgress: (bytesUploaded, bytesTotal) => {
|
|
|
+ const percentage = Math.round((bytesUploaded / bytesTotal) * 100)
|
|
|
+ onProgress?.(percentage)
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ resolve()
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ upload.start()
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 播放相关 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取视频播放信息
|
|
|
+ */
|
|
|
+ getPlaybackInfo(videoId: string): VideoPlaybackInfo {
|
|
|
+ const domain = this.getCustomerDomain()
|
|
|
+ return {
|
|
|
+ uid: videoId,
|
|
|
+ preview: `https://${domain}/${videoId}/watch`,
|
|
|
+ thumbnail: `https://${domain}/${videoId}/thumbnails/thumbnail.jpg`,
|
|
|
+ playback: {
|
|
|
+ hls: `https://${domain}/${videoId}/manifest/video.m3u8`,
|
|
|
+ dash: `https://${domain}/${videoId}/manifest/video.mpd`
|
|
|
+ },
|
|
|
+ iframe: `https://${domain}/${videoId}/iframe`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成带时间戳的缩略图 URL
|
|
|
+ */
|
|
|
+ getThumbnailUrl(videoId: string, options?: {
|
|
|
+ time?: string // 时间,如 "1s", "5m30s"
|
|
|
+ height?: number // 高度
|
|
|
+ width?: number // 宽度
|
|
|
+ fit?: 'crop' | 'clip' | 'scale' | 'fill'
|
|
|
+ }): string {
|
|
|
+ const domain = this.getCustomerDomain()
|
|
|
+ const params = new URLSearchParams()
|
|
|
+ if (options?.time) params.set('time', options.time)
|
|
|
+ if (options?.height) params.set('height', options.height.toString())
|
|
|
+ if (options?.width) params.set('width', options.width.toString())
|
|
|
+ if (options?.fit) params.set('fit', options.fit)
|
|
|
+ const queryString = params.toString()
|
|
|
+ return `https://${domain}/${videoId}/thumbnails/thumbnail.jpg${queryString ? '?' + queryString : ''}`
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成动图预览 URL
|
|
|
+ */
|
|
|
+ getAnimatedThumbnailUrl(videoId: string, options?: {
|
|
|
+ start?: string // 开始时间
|
|
|
+ end?: string // 结束时间
|
|
|
+ height?: number
|
|
|
+ width?: number
|
|
|
+ fit?: 'crop' | 'clip' | 'scale' | 'fill'
|
|
|
+ fps?: number
|
|
|
+ }): string {
|
|
|
+ const domain = this.getCustomerDomain()
|
|
|
+ const params = new URLSearchParams()
|
|
|
+ if (options?.start) params.set('start', options.start)
|
|
|
+ if (options?.end) params.set('end', options.end)
|
|
|
+ if (options?.height) params.set('height', options.height.toString())
|
|
|
+ if (options?.width) params.set('width', options.width.toString())
|
|
|
+ if (options?.fit) params.set('fit', options.fit)
|
|
|
+ if (options?.fps) params.set('fps', options.fps.toString())
|
|
|
+ const queryString = params.toString()
|
|
|
+ return `https://${domain}/${videoId}/thumbnails/thumbnail.gif${queryString ? '?' + queryString : ''}`
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 直播管理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取直播输入列表
|
|
|
+ */
|
|
|
+ async listLiveInputs(): Promise<CloudflareListResponse<CloudflareLiveInput>> {
|
|
|
+ return this.client.get('/live_inputs')
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建直播输入
|
|
|
+ */
|
|
|
+ async createLiveInput(params?: CreateLiveInputParams): Promise<CloudflareApiResponse<CloudflareLiveInput>> {
|
|
|
+ return this.client.post('/live_inputs', params || {})
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取直播输入详情
|
|
|
+ */
|
|
|
+ async getLiveInput(liveInputId: string): Promise<CloudflareApiResponse<CloudflareLiveInput>> {
|
|
|
+ return this.client.get(`/live_inputs/${liveInputId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新直播输入
|
|
|
+ */
|
|
|
+ async updateLiveInput(liveInputId: string, params: CreateLiveInputParams): Promise<CloudflareApiResponse<CloudflareLiveInput>> {
|
|
|
+ return this.client.put(`/live_inputs/${liveInputId}`, params)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除直播输入
|
|
|
+ */
|
|
|
+ async deleteLiveInput(liveInputId: string): Promise<CloudflareApiResponse<null>> {
|
|
|
+ return this.client.delete(`/live_inputs/${liveInputId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取直播输入的播放信息
|
|
|
+ */
|
|
|
+ getLivePlaybackInfo(liveInputId: string): VideoPlaybackInfo {
|
|
|
+ const domain = this.getCustomerDomain()
|
|
|
+ return {
|
|
|
+ uid: liveInputId,
|
|
|
+ preview: `https://${domain}/${liveInputId}/watch`,
|
|
|
+ thumbnail: `https://${domain}/${liveInputId}/thumbnails/thumbnail.jpg`,
|
|
|
+ playback: {
|
|
|
+ hls: `https://${domain}/${liveInputId}/manifest/video.m3u8`,
|
|
|
+ dash: `https://${domain}/${liveInputId}/manifest/video.mpd`
|
|
|
+ },
|
|
|
+ iframe: `https://${domain}/${liveInputId}/iframe`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 直播输出(转推) ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取直播输出列表
|
|
|
+ */
|
|
|
+ async listLiveOutputs(liveInputId: string): Promise<CloudflareListResponse<CloudflareLiveOutput>> {
|
|
|
+ return this.client.get(`/live_inputs/${liveInputId}/outputs`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建直播输出(转推到其他平台)
|
|
|
+ */
|
|
|
+ async createLiveOutput(liveInputId: string, params: {
|
|
|
+ url: string
|
|
|
+ streamKey: string
|
|
|
+ enabled?: boolean
|
|
|
+ }): Promise<CloudflareApiResponse<CloudflareLiveOutput>> {
|
|
|
+ return this.client.post(`/live_inputs/${liveInputId}/outputs`, params)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除直播输出
|
|
|
+ */
|
|
|
+ async deleteLiveOutput(liveInputId: string, outputId: string): Promise<CloudflareApiResponse<null>> {
|
|
|
+ return this.client.delete(`/live_inputs/${liveInputId}/outputs/${outputId}`)
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 录像管理 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取直播录像列表
|
|
|
+ */
|
|
|
+ async listLiveRecordings(liveInputId: string): Promise<CloudflareListResponse<CloudflareVideo>> {
|
|
|
+ return this.client.get(`/live_inputs/${liveInputId}/videos`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 创建默认实例(需要在使用前配置)
|
|
|
+let streamApiInstance: CloudflareStreamApi | null = null
|
|
|
+
|
|
|
+/**
|
|
|
+ * 初始化 Cloudflare Stream API
|
|
|
+ */
|
|
|
+export function initCloudflareStream(config: CloudflareStreamConfig): CloudflareStreamApi {
|
|
|
+ streamApiInstance = new CloudflareStreamApi(config)
|
|
|
+ return streamApiInstance
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 获取 Cloudflare Stream API 实例
|
|
|
+ */
|
|
|
+export function useCloudflareStream(): CloudflareStreamApi {
|
|
|
+ if (!streamApiInstance) {
|
|
|
+ throw new Error('Cloudflare Stream API not initialized. Call initCloudflareStream first.')
|
|
|
+ }
|
|
|
+ return streamApiInstance
|
|
|
+}
|
|
|
+
|
|
|
+export { CloudflareStreamApi }
|
|
|
+export default CloudflareStreamApi
|