파이썬을 이용해서 유튜브 플레이리스트 동영상을 받아보기 3 - ffmpeg 사용 (1080p 이상 영상, 인코딩)

2019. 2. 11. 21:54Programming/Python

들어가기 전에

YouTube에 공유되는 영상들은 모두 저작권을 가지고 있는 영상입니다. 이를 다운로드 받아 무단으로 배포하거나, 저작권자의 허락 없이 임의로 수정하여 사용할 경우 법적 책임을 물을 수 있습니다. 이 포스트에서 YouTube 영상을 다운로드 받는 방법을 설명하고는 있으나, 이에 대해 발생하는 문제에 대해서는 저는 책임지지 않습니다.

Pytube로 1080p 이상 동영상 받기

지난 포스팅에서 progressive와 DASH(or adaptive) 방식을 전부 소개했습니다. progressive는 video와 audio가 전부 들어간 영상을 받을 수 있지만, 최대 720p까지밖에 다운로드가 안되는 문제점이 있다고 말했습니다. 대신 DASH(adaptive) 방식은 720p 이상의 영상들도 받을 수 있지만, video만 있는 파일과 audio만 있는 파일이 분리되어 제공되는 단점이 있다고 했습니다.

즉, pytube로 1080p 이상의 영상을 온전하게 받기 위해서는 또다른 절차가 필요합니다. video only인 파일과 audio only인 파일을 별도로 받은 후, 인코딩을 통하여 하나로 통합시키는 과정이 필요합니다.

ffmpeg 사용하기

동영상과 관련된 코딩을 하다보면 꼭 듣게 되는 이름이 ffmpeg입니다. 오픈소스 기반의 강력한 영상 인코더로, 미디어를 재생하는 앱이나 영상을 처리하는 앱들 중에 이 ffmpeg이 들어간 경우를 굉장히 많이 볼 수 있습니다. 성능은 좋으나 gui를 지원하지 않고 프롬프트 상의 cli로만 작동하므로 일반인들에겐 그렇게 친절한 프로그램은 아닙니다. 그러나 우리같은 개발자들에게 cli로 작동하는 인코더야말로 가장 필요한 인코더라고 할 수 있겠죠.

ffmpeg 공식 홈페이지

설치 방법은 쉘 환경에 익숙하지 못한 분들에겐 조금 복잡합니다. 설치 후에 환경변수에서 path 조정까지 해줘야하니.. 자세한 설치 방법은 구글링 해보세요.

이 포스팅은 ffmpeg을 소개하는 데에 목적이 있는 것이 아니므로, 사용 방법까지 소개하진 않겠습니다.

Python에서 다른 프로세스 실행하기 - subprocess

pytube에서 다운로드 받은 파일들은 인코딩하기 위해서 ffmpeg을 호출해야합니다. 다운로드만 Python 상에서 끝내고, 인코딩을 ffmpeg을 직접 실행하는 방식의 노가다도 해볼 수 있겠습니다만 귀찮으니까요.

Python에서 다른 프로세스를 실행하기 위해선 subprocess 모듈을 불러와야 합니다.

import subprocess

subprocess의 공식 API 문서는 여기를 참조하세요.

이 포스팅에선 subprocess 중에서 Popen을 사용하겠습니다. 이유는.. 찾아보니 다른 메소드들도 전부 Popen을 상속해서 쓰는 것이고, 세밀한 조정은 Popen이 적합하다고 해서입니다. 자세한 사항은 저도 모릅니다 호호

subprocess의 Popen()을 사용하는 예제를 보겠습니다.

>>> import subprocess
>>> p = subprocess.Popen(\['echo', 'hello', 'world!'\])
hello world!

Popen의 첫번째 인자는 args로, list의 형태로 받을 수 있습니다. 위는 쉘 명령어 echo hello world!를 실행하는 구문입니다. echo 뒤에 전달된 인자들을 터미널 상에 띄우는 명령어입니다. 이 경우엔 stdout이 파이썬의 인터프리터이므로 파이썬 화면에 그대로 표시되었습니다.

그러나 인터프리터 상에 subprocess의 실행결과가 그대로 노출되는 것이 거슬립니다. echo 한 줄 정도는 괜찮지만, ffmpeg을 실제로 실행하면 처리 과정이 굉장히 장황하게 중계가 됩니다. pytube로 받은 파일이 여러개면 인터프리터의 스크롤이 쭉쭉 내려가서 결과 확인하기가 난감할 것입니다.

다음은 Popen()에서의 표준 출력 stdout을 인터프리터가 아니라 다른 곳으로 옮겨 사용하는 예제입니다.

>>> import subprocess
>>> p = subprocess.Popen(['echo', 'hello', 'world!'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
>>> out, err = p.communicate()
>>> code = p.returncode
>>> out
b'hello world\\n'
>>> err
b''
>>> code
0

보시다시피 pipe를 사용했습니다. subprocess.Popen()은 인자로 전달된 프로세스를 실행하면서 Popen 클래스 객체를 만들어 return합니다. 생성할 때, stdout과 stderr를 PIPE로 별도로 지정해주면 프로세스의 표준출력은 인터프리터에 표시되지 않고 Popen 객체가 그대로 가지고 있게 됩니다.

PIPE를 통해 인터프리터에 출력하지 않게 만드는 것은 성공했습니다. 그러나 실행된 프로세스의 결과가 어떠한지, 에러 같은 것이 발생하진 않았는지도 확인해야합니다. communicate()는 Popen 객체가 가지고 있는 stdout, stderr를 하나의 튜플 형태로 반환해주며, returncode는 Popen 객체가 실제로 실행한 프로세스의 exitcode를 담고 있습니다. 0이라면 정상 종료, -1 같은 다른 값이라면 무엇인가 문제가 생겨서 강제 종료되었다는 의미겠죠.

ffmpeg과 subprocess 같이 사용하기

다음은 ffmpeg으로 video only 파일과 audio only 파일을 하나로 합치는 명령어입니다.

$ ffmpeg -i video.mp4 -i audio.mp4 result.mp4

참 고맙게도 vcodec과 acodec만 가지고 있는 파일일 경우엔, 그냥 -i를 써서 명시만 해줘도 알아서 합쳐주더군요. 보면 아시겠지만 video.mp4는 video only 파일, audio.mp4는 audio only 파일의 이름이며, -i가 앞에 붙은 것은 input, 즉 source라서 그렇습니다. 마지막 result.mp4는 인코딩된 결과물 파일의 이름을 지정해준 것입니다.

이제 이 명령어를 Python 상의 subprocess로 구현을 해보겠습니다.

import subprocess
import os

workdir = os.path.dirname(os.path.realpath(__file__))

result = subprocess.Popen(['ffmpeg', '-y', '-i', workdir + '/video.mp4', '-i', workdir + '/audio.mp4', workdir + '/' + result + '.mp4'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = result.communicate()
exitcode = result.returncode
if exitcode != 0:
    print(exitcode, out.decode('utf8'), err.decode('utf8'))
else:
    print('Completed')

쉘에서 실행하는 것과 다른 점이 몇개가 있습니다. 일단, -i의 파일이 어떤 파일을 지시하는 지 애매모호하므로 아예 절대경로로 지정하도록 했습니다. workdir은 현재 python 스크립트가 실행되고 있는 폴더를 의미합니다.

또한, ffmpeg 바로 다음에 -y도 추가되었습니다. 쉘에서 ffmpeg을 실행할 때에는 사용자에게 yes or no로 대답하는 질문을 하나 던집니다. subprocess로 실행할 경우엔 -y를 붙여서 이 질문을 스킵시켜야 합니다.

완성본

완성된 코드는 다음과 같습니다.

 

실제로 구동해본 결과, 1080p로 소리도 이상없이 잘 재생됩니다.