话不多说,干就完了!今天我们来了结下视频网这部分,上篇实现了获取真实的m3u8的视频地址,那这篇我们就来实现如何下载m3u8文件,如何合并成一个整的并且支持web播放的MP4文件,其实这里也有很多文章讲过这部分功能的实现,不过在合并ts文件成MP4的时候,大部分使用的系统的copy命令,但是这样生成的MP4是无法在web网页中播放的,跟我之前序章里的思路是一样,还得通过ffmpeg在转成H264视频编码格式,那还不如直接就用ffmpeg合并成MP4,合并后的文件就支持web网页播放。
所以下面我们就来实现下载ts文件并通过用ffmpeg合并成mp4,首先我贴一下全局变量:
#初始化函数
def __init__(self):
#视频地址
self.videourl=''
#视频名称
self.videoname = ''
self.pronum = 4
#超时时间
self.timeout = 60
#当前下载成功的文件数
self.down_file_num=0
#下载的文件总数
self.filenum=0
#记录下载失败的地址集合
self.failurls=[]
#视频保存的文件夹位置
self.filefolder="app\\static\\video\\"
#视频log日志的文件夹位置
self.logsfolder = "app\\static\\logs\\"
接着,因为访问页面,我们点击下载就会调用此方法,那我们就来看下这段下载m3u8的方法代码:
#下载m3u8视频
def down_video(self,videourl,videoname):
#清空此文件日志
self.clear_log(videoname)
#开始写入日志
self.write_log(videoname,'--------------------------开始下载----------------------------')
# 初始化参数
self.video_init()
#获得真实的m3u8地址
murl=self.get_video_m3u8(videourl,videoname)
response = requests.get(murl,timeout=self.timeout)
#获取m3u8中ts文件的集合
videofiles=re.findall(r',\n(.*?)\.ts',response.text)
#获取视频地址的前缀
fronturl = murl[:murl.rfind("/")]
#赋值全局变量文件的个数
self.filenum=len(videofiles)
#写入日志
self.write_log(videoname,'获取文件总数: {} 个'.format(self.filenum))
#开启线程,这里没加join是让其在后台异步运行
t = threading.Thread(target=self.run_down, args=(fronturl,videofiles))
t.start()
这里主要说下上面用到的一些方法,
clear_log这个是清空文件日志的方法,这里我的目的就是实现如果下载视频没有完成则清空重新下载,代码如下:
#清空日志
def clear_log(self,fname):
with open("{}{}.txt".format(self.logsfolder,fname), "w",encoding="utf-8") as f:
f.write('')
f.close()
那接着这个write_log方法就是根据对应的文件名写入日志,从来实现前端页面的下载进度展示,这里是个通用方法,代码如下:
#写入日志文件
def write_log(self,fname,txt):
with open("{}{}.txt".format(self.logsfolder,fname), "a+",encoding="utf-8") as f:
f.write(txt+'\n
')
f.close()
video_init 这里是初始化参数:
# 初始化参数
def video_init(self):
self.down_file_num = 0
self.filenum = 0
线程中run_down方法,里面使用了进程池来利用多核实现多任务下载,因为在网页端使用了进程池的pool.join(),所以我们为了防止请求阻塞页面,所以才开启一个线程来执行,代码如下:
#开启一个线程来执行,防止请求阻塞
def run_down(self,fronturl,videofiles):
# 定义进程池
pool = Pool(processes=self.pronum)
for ts in videofiles:
#这里我们判断如果ts文件包含http的则说明是完整的路径
if 'http' in ts:
tsurl = ts + ".ts"
tsname = tsurl.split("/")[-1].split(".ts")[0]
else:
tsurl = fronturl + "/" + ts + ".ts"
tsname = ts
pool.apply_async(self.down_video_post, (tsurl, tsname), callback=self.callnum)
pool.close() # 关闭进程池,不在让往进程池中添加进程
pool.join()
一环套一环,down_video_post这个方法呢
# 下载m3u8视频的方法
def down_video_post(self,url,fname):
# 获取ts文件二进制数据
try:
ts = requests.get(url,timeout=self.timeout)
tscon=ts.content
with open("{}".format(self.filefolder)+fname+".ts", "wb") as f:
f.write(tscon)
self.write_log(self.videoname,"下载完成:{}".format(url))
f.close()
return ""
except Exception as e:
print(e)
return url
这里其实很好理解,就是将ts文件下载并保存下来,这里如果是下载失败则返回当前的下载链接,如果还要想深入的同学,这里还可以实现下载失败的处理,可以再次尝试下载什么的,还可以使用文件偏移来实现文件下载进度的功能等等,这里就不做过多的介绍了,pool.apply_async中的callback顾名思义就是获取到单个进程的返回结果,callnum方法则是处理返回结果的方法:
#回调函数判断是否所有文件下载完成
def callnum(self,msg):
#下载任务完成则加1
self.down_file_num += 1
self.write_log(self.videoname,"当前剧集:{},已下载完成{}个文件".format(self.videoname,self.down_file_num))
#这里表示如果msg不为空则说明当前地址下载失败,我们插入到失败的集合里
if msg!="":
self.write_log(self.videoname,"{} 下载失败".format(msg))
self.failurls.append(msg)
#这里判断的是如果下载任务的文件数等于总文件数,则说明下载完成
if self.down_file_num == self.filenum:
print("所有文件下载完成")
print(self.failurls)
self.write_log(self.videoname,"-------------------------所有文件下载完成----------------------")
#合并生成MP4
self.merge_video()
合并MP4的方法我们用了ffmpeg来实现,怎么安装怎么使用,有兴趣的同学可以仔细去研究下,这里因为我是部署在window系统上的,linux中也有对应的插件,在我这个项目中无需安装我已经集成好了,代码如下:
#合并视频并转成MP4
def merge_video(self):
#获取存放ts文件夹下的所有ts文件
filelist=os.listdir(self.filefolder)
#ffmpeg合并ts文件目录文件
filestr = 'app\\static\\videomp4\\filelist.txt'
#写入到上面的文件路径中
with open(filestr, "w",encoding='utf-8') as f:
tstr=''
for fname in filelist:
tstr+="file '{}'\n".format(self.filefolder+fname)
f.write(tstr)
f.close()
#合并命令,详细请自行参考ffmpeg命令参数
shell_str='app\\video\\ffmpeg.exe -y -f concat -safe 0 -i {} -c copy "app\\static\\videomp4\\{}.mp4"'.format(filestr,self.videoname)
p = subprocess.Popen(shell_str, shell=True)
#p = subprocess.Popen(shell_str, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,encoding="utf-8",universal_newlines=True)
p.wait()
retcode=p.returncode
print(retcode)
if retcode==0:
self.write_log(self.videoname,"文件合并成功")
#删除文件夹下ts文件
del_cmd = 'del {}*.ts'.format(self.filefolder)
p = subprocess.Popen(del_cmd, shell=True,stdin=subprocess.PIPE,stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.wait()
retcode = p.returncode
if retcode == 0:
self.write_log(self.videoname,"成功删除ts文件")
url="{}/static/videomp4/{}.mp4".format('http://127.0.0.1:5000',self.videoname)
self.write_log(self.videoname,'合并后mp4文件路径:{} 播放'.format(url,self.videoname,url))
self.write_log(self.videoname,'状态:全部下载完成')
详细介绍都在注释上,上面就是利用subprocess来调用ffmpeg.exe文件的,p = subprocess.Popen(shell_str, shell=True)这里我是为了便于观察在合并的时候是否有问题,就让它在控制台打印执行的信息,所以这里没有加stdout=subprocess.PIPE, stderr=subprocess.PIPE,如果不想在控制台打印信息则启用下面注释的代码,执行成功则会返回0,失败会返回1,合并成功后我会清除掉下载的ts文件以节省资源的开销。
在main主文件里调用方法如下:
#下载
@app.route("/download",methods=["post"])
def download():
args = request.args if request.method == 'GET' else request.form
name = args.get('name', "", type=str)
url = args.get('url', "", type=str)
num = args.get('num', 0, type=int)
#判断当前要下载的剧集的日志文件是否存在
filedir = "app\\static\\logs\\{}.txt".format(name)
if os.path.isfile(filedir):
with open(filedir, 'r', encoding='utf-8') as f:
logs = f.read()
if "全部下载完成" in logs:
status = 1
else:
if num==0:
v = Video()
v.down_video(url, name)
print("下载任务提交成功")
msg = {"code": 200, "msg": "下载任务提交成功,准备下载中……"}
return jsonify(msg)
else:
status = 0
msg = {"code": 200,"status":status, "msg": logs}
else:
v = Video()
v.down_video(url,name)
print("下载任务提交成功")
msg={"code":200,"msg":"下载任务提交成功,准备下载中……"}
return jsonify(msg)
这里有个逻辑就是判断当前前端页面点击下载对应的剧集的日志文件是否存在,如果不存在则直接开始下载任务,如果存在,又分2种情况,因为前端页面是用轮询请求这个接口来判断是否完成的,如果在文件中匹配到“全部下载完成”则说明该文件已下载完成,如果没有则继续请求,这里加了num这个参数,是防止你在下载的过程中频繁点击下载按钮而造成过多任务执行。
以上部分就是实现下载m3u8并合并成MP4的逻辑代码,以上只是按我自己的思路封装了这些方法,里面还有很大的优化空间,这里值得注意的是,因为这里区别于C端的开发,在网页端执行代码,类的全局变量只对当次操作有效,如果要运用到全局,你可以发到session、cookie或者本地存储里。实现开发视频网这个过程运用到了前几篇所讲的知识,其实无论开发什么,最重要的就是首先要理清思路及流程,这样实现起来才事半功倍!
学习的步伐永不停止,时间又过了一天,头发又少了几根,不管是岁月蹉跎了我,还是我蹉跎了岁月,我还是那为自己梦想奋斗的老李,江湖难说再见,有人的地方就是江湖,咱们下篇见!
需要完整源码的童鞋们请关注公众号回复:视频网
关注公众号,超越平凡才能成就自我