Tube - Video thumbnails

54 阅读3分钟
  • uploadthing - File Uploader
    • 视频列表中的缩略图一直使用的是Mux自动生成的,现在我们需要实现支持用户上传thumbnail重置thumbnail
    • Next.js App Router Setup 请参考此文档进行安装和配置,版本信息:"uploadthing": "^7.7.4""@uploadthing/react": "^7.3.3"
  • 管理上传图片
    • 在视频详情表单中,我们给视频thumbnail提供了3个操作,分别是 ChangeAI-generatedRestore
    • 视频的默认缩略图由mux提供的,所以 Restore 即恢复mux提供的缩略图,Change则对应的是用户上传新的缩略图,AI-generated我们后面会说到,是使用AI工具生成一张缩略图
    • 这里涉及到的图片管理,也就是我们在以上过程中需要及时更新数据库所保存的文件且避免重复
    • 不依赖上传文件的 file url,而是使用 file key 来进行文件管理
    • 当Mux自动生成缩略图时,我们也一样将图片保存至uploadthing中,并且在数据库中同步对应的url和key(统一文件的管理方式,在Mux Webhook中、restore缩略图时添加这部分逻辑)
    1. src/db/schema.ts ,在videos表结构中添加以下字段
    export const videos = pgTable("videos", {
      ...
      thumbnailUrl: text('thumbnail_url'), // 缩略图
      thumbnailKey: text('thumbnail_key'), // 缩略图存储的key
      previewUrl: text('preview_url'),  // 预览
      previewKey: text('preview_key'), // 预览存储的key
      ...
    })
    
    1. src/app/api/uploadthing/core.ts,在uploadthing中间件中,先删除uploadingthing中旧的缩略图信息,再删除数据库中的旧的缩略图信息
    import { UploadThingError, UTApi } from "uploadthing/server";
    ...
    export const ourFileRouter = {
      thumbnailUploader: 
        ...
        .middleware(async ({ input }) => {
          ...
          // 如果视频已存在thumbnailKey,删除旧的thumbnail
          if (exitingVideo.thumbnailKey) {
            const utApi = new UTApi() 
            await utApi.deleteFiles(exitingVideo.thumbnailKey)  // 清除uploadthing旧的thumbnail
          
            await db
              .update(videos)
              .set({ thumbnailKey: null, thumbnailUrl: null }) // 清除数据库中旧的thumbnail
              .where(...)
          }
          ...
       })    
    } satisfies FileRouter;
    
    1. src/modules/videos/server/procedures.tsrestoreThumbnail 接口中也一样,删除uploading信息、删除数据库中的信息
     与上一步逻辑相同
    
    1. src/app/api/videos/webhook/route.ts Mux自动生成的缩略图,也一样保存在uploadthing中
    case "video.asset.ready": {
      ...
      ...
      const tempThumbnailUrl = `https://image.mux.com/${playbackId}/thumbnail.jpg`
      const tempPreviewUrl = `https://image.mux.com/${playbackId}/animated.gif`
      const duration = data.duration ? Math.round(data.duration * 1000) : 0;
      
      const utApi = new UTApi();
      const [uploadThumbnail, uploadPreview] = await utApi.uploadFilesFromUrl([tempThumbnailUrl, tempPreviewUrl])
      if(!uploadThumbnail.data || !uploadPreview.data) {
          return new Response("Failed to upload thumbnail or preview", { status: 500 })
      }
      const { key: thumbnailKey, ufsUrl: thumbnailUrl } = uploadThumbnail.data
      const { key: previewKey, ufsUrl: previewUrl } = uploadPreview.data 
      
        await db
          .update(videos)
          .set({
            ...
            thumbnailUrl,
            thumbnailKey,
            previewUrl,
            previewKey,
            ...
          })
    }
    
    1. src/modules/videos/server/procedure.tsrestoreThumbnail 接口中也一样
    与上一步逻辑相同
    
  • 关于上传预览gif的坑
    • 理想状态下,我们想统一文件的管理方式,也就是所有视频缩略图统一由uploadthing保存
    • 因此,以上代码在mux的webhook中要通过文件上传的方式将图片上传至uploadthing(mux通过url下载图片,再将图片上传)
    • 实际情况是,这个下载上传的过程非常耗时且不稳定(尤其是gif图),远远超出了mux要求的5s内事件响应,最终导致mux反复执行事件响应,而反复重复的上传图片
    • 使用 setImmediate() 处理异步任务,他是Node.js环境中专有的一个全局方法,将回调函数排入事件循环中称为check阶段,setImmediate(callback[, ...args])
// thumbnail - 确保异步操作不会阻塞事件循环 

setImmediate(async () => {
  try {
    // 上传至uploadthing
    const uploadThumbnail = await utApi.uploadFilesFromUrl(tempThumbnailUrl)

    if(!uploadThumbnail.data?.ufsUrl) {
      return logger.error("Failed to upload thumbnail")
    }

    const { key: thumbnailKey, ufsUrl: thumbnailUrl } = uploadThumbnail.data

    await db
      .update(videos)
      .set({ thumbnailUrl, thumbnailKey })
      .where(eq(videos.muxUploadId, data.upload_id!)) // data.upload_id! 表示非空断言
  }catch(err) {
    logger.error('Error processing video.asset.ready webhook:', err)
  }
})