前言
在 Day1 的進度中,製作完圖形視窗介面後,現在的 Youtube 影片下載程式還只是一個空有 GUI 樣子的空殼,後面再增加下載影片的功能,這篇會先透過 python 的 pytube 套件,測試如何下載第一個 Youtube 影片之後,再來把下載影片的功能添加到 Day1 製作的圖形視窗介面中。
需要安裝的套件
可以在 pytube 的 GitHub 頁面找到 Installation 下寫的安裝指令(現在已經變成 pytube3 了)
在命令提示字元(cmd)中輸入以下指令pip install pytube3 --upgrade
,來安裝 pytube 套件,看到Successfully installed pytube3-9.6.4 就表示安裝完成了,如下:
到目前為止pytube的最新版本為pytube3-9.6.4。
然後在命令提示字元(cmd)中輸入python
之後,在輸入from pytube import YouTube
,來測試套件是否正常,如果沒報錯就沒問題。
測試下載影片-1
根據 pytube 的 GitHub 頁面上 Quick start 寫的,我們可以簡單的透過以下三行程式碼,下載第一個 Youtube 影片。
from pytube import YouTube
link = 'https://youtu.be/9bZkp7q19f0' #想下載的影片連結
YouTube(link).streams.get_highest_resolution().download() #下載影片
我們以下載 PSY - GANGNAM STYLE(강남스타일) M/V 這部影片為例子,執行完以上三行程式碼後,你可能會發現只下載到了 640x360p 的版本(以上面那張圖的狀況為例),這是因為 Youtube 現在有兩種傳輸方式, HTTP 漸進式下載(HTTP Progressive Download)和基於HTTP的動態自適應流(Dynamic Adaptive Streaming over HTTP,縮寫DASH,也稱MPEG-DASH),目前是以 DASH 為主,而 .get_highest_resolution() 預設好像是抓 Progressive 的,這部影片的 Progressive 就只有提供 360p 的選項。
(123)
如果用 .filter(progressive=True) 把 Progressive 的選項印出來(如上圖),結果也是相同的。
漸進式下載和自適應下載的差別
有人可能會想問,這兩種有什麼不同之處?
HTTP 漸進式下載,根據中文百科上寫的:(我只擷取部分)
漸進式下載是介於下載後本地播放與實時流媒體之間的一種播放方式,下載後本地播放必須將檔案全部下載完成後才能播放,而漸進式下載不必等到全部下載完成後再播放,它可以一邊下載一邊播放,完成播放內容之後,整個檔案會保存在計算機上。從播放的效果和用戶體驗上看,漸進式下載和實時流媒體是一樣的,不同的是漸進式下載會在本地保留檔案的副本,因此有人把它稱為“偽流媒體”,即不是真正意思上的“流媒體”,此外,漸進式下載不能跳過某些數據包進行下載。
不像流媒體伺服器,幾乎都只傳送差不多十秒鐘的數據給客戶端。HTTP WEB伺服器會在媒體檔案下載完成之前一直在傳送數據流。如果一開始播放時你就暫停了一個漸進式下載的視頻,然後在那等著,就會把整視頻個檔案都下載到瀏覽器的快取裡面,這樣就可以不停頓、平滑地把整個視頻都看完。用這樣的下載的方式,一個已經完全下載了的十分鐘的視頻,就有可能你只看了三十秒鐘,因為你並不喜歡這段視頻,然後關掉它,其實這樣你和你的內容提供商都浪費了九分三十秒的寬頻。
而根據維基百科上寫的,基於HTTP的動態自適應流:
MPEG-DASH會將內容分解成一系列小型的基於HTTP的檔案片段,每個片段包含很短長度的可播放內容,而內容總長度可能長達數小時(例如電影或體育賽事直播)。內容將被製成多種位元速率的備選片段,以提供多種位元速率的版本供選用。當內容被MPEG-DASH客戶端回放時,客戶端將根據目前網路條件自動選擇下載和播放哪一個備選方案。客戶端將選擇可及時下載的最高位元速率片段進行播放,從而避免播放卡頓或重新緩衝事件。也因如此,MPEG-DASH客戶端可以無縫適應不斷變化的網路條件並提供高品質的播放體驗,擁有更少的卡頓與重新緩衝發生率。
可以看到漸進式下載會在完成播放之後,把整個檔案會保存在本地端。因此,即使用戶僅在觀看三十秒鐘後就離開,無論如何,整個文件都會通過YouTube服務器傳遞,浪費傳輸流量。
這也是為什麼 Youtube 和各大影音網站採用 HTTP 動態自適應流傳輸的原因之一,這同時也是漸進式下載的優點,因為可以比較容易取得完整的影音檔。
而動態自適應流下載則可以根據客戶端目前的網路條件自動選擇下載和播放對應位元速率的版本,減少播放卡頓或重新緩衝的概率,雖然優點很多,不過對製作 Youtube 影片下載程式來說比較麻煩,因為影片檔案被拆分成,影片和聲音兩個檔案,所以要自己把載下來的兩個檔案合併。
測試下載影片-2
現在我們來測試下載 PSY - GANGNAM STYLE(강남스타일) M/V 這部影片的 1080p 版本。
接著會用到 FFmpeg 來合併影片和聲音檔,記得先到 FFmpeg 的官方網站下載 Windows 64-bit Static 的版本,並解壓縮到跟程式碼相同位置的資料夾,這樣才不會報錯。
而這裡為了方便(絕對不是我懶)我借用了超圖解出書網站 上寫的 tube.py 原始碼中的部分函式,將 測試下載影片-1 中的程式碼改寫後如下:
from pytube import YouTube
import os
import subprocess
fileobj = {}
download_count = 1
# 檢查影片檔是否包含聲音
def check_media(filename):
r = subprocess.Popen([".\\bin\\ffprobe", filename],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
out, err = r.communicate()
if (out.decode('utf-8').find('Audio') == -1):
return -1 # 沒有聲音
else:
return 1
# 合併影片檔
def merge_media():
temp_video = os.path.join(fileobj['dir'], 'temp_video.mp4')
temp_audio = os.path.join(fileobj['dir'], 'temp_audio.mp4')
temp_output = os.path.join(fileobj['dir'], 'output.mp4')
cmd = f'".\\bin\\ffmpeg" -i "{temp_video}" -i "{temp_audio}" \
-map 0:v -map 1:a -c copy -y "{temp_output}"'
try:
subprocess.call(cmd, shell=True)
# 視訊檔重新命名
os.rename(temp_output, os.path.join(fileobj['dir'], fileobj['name']))
os.remove(temp_audio)
os.remove(temp_video)
print('視訊和聲音合併完成')
except:
print('視訊和聲音合併失敗')
def onProgress(stream, chunk, remains):
total = stream.filesize
percent = (total-remains) / total * 100
print('下載中… {:05.2f}%'.format(percent), end='\r')
def download_sound():
try:
yt.streams.filter(type="audio").first().download()
except:
print('下載影片時發生錯誤,請確認網路連線和YouTube網址無誤。')
return
# 檔案下載的回呼函式
def onComplete(stream, file_path):
global download_count, fileobj
fileobj['name'] = os.path.basename(file_path)
fileobj['dir'] = os.path.dirname(file_path)
print('\r')
if download_count == 1:
if check_media(file_path) == -1:
print('此影片沒有聲音')
download_count += 1
try:
# 視訊檔重新命名
os.rename(file_path, os.path.join(
fileobj['dir'], 'temp_video.mp4'))
except:
print('視訊檔重新命名失敗')
return
print('準備下載聲音檔')
download_sound() # 下載聲音
else:
print('此影片有聲音,下載完畢!')
else:
try:
# 聲音檔重新命名
os.rename(file_path, os.path.join(
fileobj['dir'], 'temp_audio.mp4'))
except:
print("聲音檔重新命名失敗")
# 合併聲音檔
merge_media()
yt = YouTube('https://youtu.be/9bZkp7q19f0', on_progress_callback=onProgress,
on_complete_callback=onComplete)
yt.streams.filter(subtype='mp4',resolution="1080p")[0].download()
大概解釋一下,上方的程式碼,是先用 .filter(subtype='mp4',resolution="1080p") 從可使用的影片流中過濾出 1080p 的影片 mp4 檔之後,用 .download() 把無音軌的純影片檔載下來,過程中會不斷執行 onProgress 顯示下載進度,並在下載完成後自動觸發 onComplete 函式,檢查影片是否有聲音,如果沒聲音的話,就會把視訊檔重新命名為 temp_video.mp4 ,執行 download_sound 函式下載聲音檔,並把聲音檔重新命名為 temp_audio.mp4 ,最後再執行 merge_media() 函式把視訊檔和聲音檔合併,然後重新命名為原本的名字。
沒問題的話,應該可以在跟程式碼相同位置的資料夾找到完整的影片檔。
小結
Day02的進度:
在測試下載影片-1中用官方給的代碼下載到了解析度 360p 的影片,了解 Youtube 漸進式傳輸和自適應傳輸的差別,並在測試下載影片-2中了解如何用 python 搭配 FFmpeg 自動將下載到的高解析度影片和聲音檔合併。
如果文章中的程式碼有錯誤或是有其他想法,請麻煩在下方留言告訴我。
參考資料
中文百科-漸進式下載
YouTube是否使用自適應流式傳輸或漸進式下載進行視頻傳輸?
維基百科-基於HTTP的動態自適應流
YouTube影片下載(一):合併視訊和音軌的Python程式
FFmpeg 簡易教學