视频上传接口 "/publish/action" 的设计思路 | 青训营

174 阅读4分钟

本篇笔记旨在记录视频上传接口的实现思路。

视频结构架构

首先需要定义一个视频需要的信息,基础结构为

type Video struct {
    VideoId   int64     `gorm:"column:video_id;primary_key;NOT NULL"`
    PlayUrl   string    `gorm:"column:play_url;type:varchar(500)"`
    CoverUrl  string    `gorm:"column:cover_url;type:varchar(500)"`
    Title     string    `gorm:"column:title;type:varchar(100)"`
    UserId    int64     `gorm:"column:user_id;NOT NULL"`
    CreatedAt time.Time `gorm:"column:created_at;index"`
}

其记录了视频的唯一表示 id, 视频信息,封面图片的播放路径,标题,作者 id 以及创建时间。

接口设计

核心思路就是把上传视频的信息填充如一个 Video 的结构体中,然后把这个结构体存入数据库内。为此,已经在 http 服务启动前启动了 Mysql 中的数据库,调用接口为 global.DB.

  • 获取作者信息和标题信息

作者信息来源于登录信息,直接检索 token 获得,当然本身也有登录状态记录,里面存有相关信息。标题信息来自前端传入,具体处理为

// 获取作者 id
token := c.PostForm("token")
claims, _ := util.Gettoken(token)
userid, _ := strconv.ParseInt(claims.UserId, 10, 64)
// 获取标题
title := c.PostForm("title")

值得一提,userid 也可以用 c.PostForm("user_id") 获得(如果登录信息中放了这个 tags ),以及读取 json 就我测试来看似乎需要用 PostForm 方法,不能用 Query 方法。

  • 获取视频文件信息

直接调用接口,此处的文件获取由前端上传提供

// 获取文件
file, err := c.FormFile("data")
if err != nil {
    c.JSON(http.StatusInternalServerError, entity.Response{
        StatusCode: -1,
        StatusMsg:  "fail to upload the file.",
    })
    return
}
  • 生成视频唯一 id

这里采用的是雪花生成法,保证生成 id 的唯一性。

// 获取视频唯一标识 id
node, err := snowflake.NewNode(1)
if err != nil {
    c.JSON(http.StatusBadRequest, entity.Response{
        StatusCode: 1,
        StatusMsg:  "failed to generate snowflake for video",
    })
}
videoId := node.Generate().Int64()
  • 存放视频信息以及封面信息到指定路径

视频存放路径是 ./public/video, 封面存放路径为 ./public/cover, 经过 gin 框架中资源的定向,即 r.Static("/static", "./public"), 视频路径和封面路径分别为

http://localhost:8080/static/video/...
http://localhost:8080/static/cover/...
关于封面生成

由于封面不会直接上传,此处使用 ffmpeg 工具对视频进行抽帧生成封面图。不需要调用 ffmpeg-go 的包,可以直接调用命令行包执行相关命令。首先把 ffmpeg 下载后,加入系统变量,那么根据视频和封面存放路径可以建立抽帧函数:

// GetCoverFromVideo 根据视频生成封面图片
func GetCoverFromVideo(videoPath, coverPath string) error {
    cmd := exec.Command("ffmpeg",
        "-i", videoPath, "-r", "1",
        "-vframes", "1",
        "-f", "image2",
        coverPath,
    )
    // 相当于 shell 命令:(videoPath, coverPath 为实际路径)
    // ffmpeg -i %videoPath% -r 1 -vframes 1 -f image2 %coverPath%
​
    output, err := cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("生成封面失败:%s\n%s", err, output)
    }
​
    return nil
}

上述函数主要功能就是实现了 shell 中调用 ffmpeg 命令完成抽帧的过程。

保存视频与封面
// 生成视频存放路径与标识,使用视频 id 保证视频存放名称唯一性
name := strconv.FormatUint(uint64(videoId), 10)
videoName := name + file.Filename
coverName := name + ".jpg"
​
videoSavePath := filepath.Join(global.PATH_VIDEO, videoName)
coverSavePath := filepath.Join(global.PATH_COVER, coverName)
​
// 保存视频
err = c.SaveUploadedFile(file, videoSavePath)
if err != nil {
    c.JSON(http.StatusBadRequest, entity.Response{
        StatusCode: -1,
        StatusMsg:  "fail to save the file to the path.",
    })
    return
}
​
// 生成并保存封面
err = GetCoverFromVideo(videoSavePath, coverSavePath)
if err != nil {
    fmt.Println(err)
    c.JSON(http.StatusBadRequest, entity.Response{
        StatusCode: -1,
        StatusMsg:  "fail to create the cover.",
    })
    return
}
​
// 获得 http 播放地址
playUrl := global.HEAD_URL + c.Request.Host + global.VIDEO_URL + videoName
coverUrl := global.HEAD_URL + c.Request.Host + global.COVER_URL + coverName
  • 视频写入数据库

最后只要把上述的信息装入结构体然后写入数据库即可

video := entity.Video{
    VideoId:  videoId,
    PlayUrl:  playUrl,
    CoverUrl: coverUrl,
    Title:    title,
    UserId:   userid,
}
err = global.DB.Create(&video).Error
if err != nil {
    c.JSON(http.StatusInternalServerError, entity.Response{
        StatusCode: -1,
        StatusMsg:  "fail to add the video into SQL",
    })
    return
}

把上述代码整合成一个事务函数并且加上最后的判定输出,即可以实现 /publish/action 这个接口的实现。

总结

上述设计思路分拆了整个任务的步骤细节。就实现这一接口来说,其总体逻辑是:定义好一个能存放视频信息的结构体,然后把待上传的视频信息全部都写入一个结构体并存入数据库。有了这个大体思路,再去细化每一个元素信息的获取就会变得十分简单了。此外这里还用到了雪花生成法保证视频 id 唯一性,以及采用了 ffmpeg 抽帧技术获得视频封面,也提升了一些认知。