話不多說,幹就完了!今天我們來了結下視頻網這部分,上篇實現了獲取真實的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<br>')
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文件路徑:{} <span class="towebmp4" data-name="{}" data-url="{}">播放</span>'.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或者本地存儲裏。實現開發視頻網這個過程運用到了前幾篇所講的知識,其實無論開發什麼,最重要的就是首先要理清思路及流程,這樣實現起來才事半功倍!
學習的步伐永不停止,時間又過了一天,頭髮又少了幾根,不管是歲月蹉跎了我,還是我蹉跎了歲月,我還是那爲自己夢想奮鬥的老李,江湖難說再見,有人的地方就是江湖,咱們下篇見!
需要完整源碼的童鞋們請關注公衆號回覆:視頻網
關注公衆號,超越平凡才能成就自我