我做了一个奇奇怪怪的视频网站
Keep Team Lv4

  最近在家羊了一周,发烧、咳嗽是一样也没落下。躺在床上头晕沉沉之际,想起我还有一个《云上下载工具》呢,反正躺着也没什么事儿,就上去看看以前下发的下载任务完成得怎么样了吧。
下载完成情况
  看样子任务都顺利的完成了呢,但是下载好的视频只能通过SCP的方式从云上拷贝下来再播放,否则也仅仅只是显示一个名字而已,不过幸好我的小云上还有一个视频播放平台。于是乎一个奇奇怪怪的想法就这样冒了出来。

推流播放

  如果小云能在Aria2下载完成后,自动将视频推送给我的播放平台。那么不就可以在线观看视频了吗?不过我的小云配置低,管道小。如果将高清视频直接推送上去,不仅对主机资源消耗大,对外网带宽要求也极高。毕竟图像越清晰,单帧图像的大小越大,就越需要的带宽来进行传输。而如果想使用较低的带宽传输,那么就意味着需要降低图像的清晰度。他们三者有一个简单的大小关系:

图像清晰度 单桢图像数据大小 外网带宽大小
清晰
模糊

  看来需要在视频推流的时候做些优化调整了。

帧率、分辨率、码率与清晰度的关系

  任何一个视频文件都会有很多参数,但是最主要的三个参数是:

  • 帧率:每秒显示画面数量,数值越大视频越流畅。
  • 分辨率:图像的尺寸大小,一般以“长 x 宽”来表示。
  • 码率:每秒显示的图片进行压缩后的数据量。

  从它们的定义中,能看出帧率和分辨率与画面的清晰度并没有直接的关系。当我们以越高的帧率和越高的分辨率播放视频时除了产生巨大的数据量(帧率x分辨率)之外,似乎也并不改变视频本身的清晰度。

  而这个巨大的数据/码率则是视频中的压缩比,压缩比越大则视频画面越糟糕。码率与画面清晰程度有正相关关系。

  因此,如果原始视频是720P的分辨率,将画面放大到1080P,并以相同的帧率和码率播放,则压缩比较原始文件更大,画面变模糊。而如果原始分辨率为1080P,缩小到720P进行播放,则压缩比反而变小,视频更清晰。

  同样,如果视频分辨率保持在相同的配置下,码率越大,则压缩比越小,画面越清晰。但是所产生的文件也越大。

  上述的基本知识点,对于接下来的要做的动作非常重要,为了在小水管的云上得到最佳播放效果,需要以此为依据,在帧率、分辨率、码率三者中找到平衡点。

FFMpeg推流

  FFMPEG中支持用以下参数设置视频输出帧率、分辨率以及码率:

-r[:stream_specifier] fps (input/output,per-stream) 设置视频帧率,单位Hz
-b:v 设置视频的码率,如果仅设置这一个参数,则将以该码率为固定码率输出视频
-minrate/maxrate 最小/大码率,设置其中任意一个参数将以动态码率的形式输出视频
-vf scale 设置输出视频缩放大小

  经过不断的摸索,以下配置是比较适合我的小水管云,当然一定也会有其他的配置可以达到更好的效果。

ffmpeg -b:v 900k -maxrate 1000k -vf scale=-1:720 -r film

何时推流

  上面既已摸索出了FFMPEG推流所需的配置参数,那么剩下的就是考虑在何时触发推流了。

方案一:下载完成后即自动进行推流、切片

  利用Aria2下载完成后可自动触发指定脚本的特点,最大限度利用云计算的资源,将视频下载与切片无缝链接。待切片完成后,用户便可在任何时候进行观看。

  但是该方案也有明显的不足之处,首先切片后的文件需要占用磁盘空间,相当于下载一部电影用了两倍的空间进行保存,对于资源有限的小云来说不划算。其次还需要对分片后的数据做额外的管理,得不偿失。作为私有云,更多的使用场景是作为云端下载器,完成下载后再将文件拷贝至本地而已,从长远来看所有下载的文件都仅是临时存放在云上而已

方案二:需要观看时再触发推流

  这有点类似点播功能,在需要的时候对指定的视频进行推流播放。但是如果没有优秀的前端和后端配合,实际使用体验可能没有想象中那么美好。

  举个简单的例子:前端在播放的时候用户拖拉了一下播放进度,此时前端需要判断当前进度条被拖拉到什么位置,并转换为对应的时长,然后通过调用后端的reset接口将信息向后传递。后端收到之后再通过FFMPEG进行跳时处理,并重新开始切片。有必要的话,还需要将结果反馈给前端。

  所以要让点播具有良好的体验,就需要投入时间优化和完善配套的前后端处理逻辑。这对于我这样的个人站点来说,不是好主意。

方案三:对下载的视频做循环推流

  该方案有点像是方案一的改良版,因为是循环推流,因此并不需要时时刻刻保存完整的视频切片文件,只需要保留时间窗内的切片数据即可,这样对磁盘存储空间的压力就减少很多。同时由于是循环推流播放,那么只要等待足够的时间,总会看完一部视频,而不会因为错过了一次,就再也看不到了。更重要的是逻辑相对简单。

简易实现

  基于小云的实际情况,我选择了方案三作为推流的时机。一种简易的实现手段就是利用脚本,循环遍历指定目录下的文件,如果是视频文件,则推流给指定的服务器即可。

#! /bin/bash
function read_dir(){
  for file in `ls $1`
  do
  if [ -d $1"/"$file ]
   then
     read_dir $1"/"$file
   else
     type=${file##*.}
     if [ $type = 'mp4' ] || [ $type = 'mkv' ]
     then
       docker run --rm -it -v $1:/videos --network netbeta registry.cn-hangzhou.aliyuncs.com/ossrs/srs:encoder \
       ffmpeg \
              -re -i /videos/$file \
              -c:a aac -q:a 0.8 \
              -c:v libx264 -b:v 900k -maxrate 1000k \
              -vf scale=-1:720 \
              -r film \
              -g 3 \
              -f flv rtmp://stream/live/movie
     fi
     sleep 10
  fi
  done
}

root_dir=`pwd`
IFS=$'\n'
while true
do
  read_dir $root_dir
  sleep 5
done
IFS=$'\t\n'

  运行上述脚本后,系统将自动将下载好的视频循环推流给我的视频播放平台,从而实现循环播放的效果。如果有新的视频文件下载完成,也会在下一轮的遍历循环中发现,并加入推流当中。而我只需要在需要的时候通过浏览器访问指定的m3u8文件即可观看视频。

播放效果

  而且实际的清晰度也还能接受。

实际清晰度

前端设计

  一切进展都挺顺利,但又似乎缺了点什么。如果只有一部视频在循环播放那还行,反正不是看见片头就是看见片尾,但如果有几部视频在依次循环播放呢?每次播放都像是开盲盒一样,永远猜不到会看到哪一部视频了。我急切的需要一个能告诉我这一切的后端。

  为了减少后端对系统资源的开销,这次我选择以Python作为后端的开发语言。

####播放时刻表
  与其等到视频播放时才获取视频名称,还不如学学影院,提前规划好当天的放映时刻表,这样不仅能知道当前在播放什么,还能预知接下来会播放什么。

  要实现这样的目的,最重要的是获取视频的时长。幸运的是FFMPEG早就替我们准备好了获取工具。

  FFMPEG提供了一个子工具ffprobe,可以读取视频文件的容器信息,其中就包含有时长。

cmd = "ffprobe -show_entries format=duration -v quiet -of flat -i {}".format(path)
duration = Shell.exec(cmd,True)
duration = float(duration.split("=\"")[1].rstrip("\"\n"))

  获取到视频时长后就可以按照一天的放映时间窗口(比如早上6点至晚上11点),进行排片。

start = FilmSchedule.OPEN_TIME
while(start <= FilmSchedule.CLOSE_TIME and len(self.films.keys()) > 0 ):
    for film in self.films.keys():
        time = self.tranlateTime(start)
        self.timetable[film].append(time)
        self.playlist.append({'path':self.films[film]['path'], 'time':start})
        start += self.films[film]['duration']
        if(start > FilmSchedule.CLOSE_TIME):
            break

  代码是从后端代码(common/sys/schedule.py)中截取的一部分,大概逻辑是从OPEN_TIME开始,依次叠加所有视频的时长,每次叠加的结果均是下一个视频开始播放的时间,直到播放时间超过CLOSE_TIME。

####视频截图
  有了播放时间,如果能在前端同时展示几张视频的截图就更好了,这样就能提前看到视频的播放效果。

  而利用FFMPEG,对视频截图也是轻松的事情。

def snapshot(self, path, num):
        image = "./snap.jpg"
        timestamp = 360
        snapshotList = []
        for offset in range(num):
          cmd = " ".join(["ffmpeg", "-ss", str(timestamp), "-i", path, "-r", "1", "-frames:v", "1", "-s", "352x288", "-q:v", "2", "-f", "image2", "-y", image])
          Shell.exec(cmd, True)
          timestamp += 10*60
          with open(image, 'rb') as f:
            imgfile = f.read()
            snapshotList.append(str(base64.b64encode(imgfile), encoding='utf-8'))
        return snapshotList

  为了减少截图的大小,我将图片大小缩小到CIF尺寸,同时为了尽可能显示不一样的视频内容(连续截图,很可能是同一张画面),每次截图均向后跳转了10分钟。

最终效果

  配合上BootStrap的响应效果,最终前端显示效果如下:

大屏幕显示效果

  如果在移动设备上则显示这样的效果:

移动设备显示效果

代码已在Gitee开源,欢迎大家进行修改与改进,也期待与大家的交流。