#!/usr/bin/env python # # smooth-dl - download videos served using Smooth Streaming technology # # Copyright (C) 2010 Antonio Ospite # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # # TODO: # - Handle HTTP errors: # "Connection reset by peer" # "Resource not available" # "Gateway Time-out" # - Support more Manifest formats: # WaveFormatEx attribute instead of PrivateCodecdata # 'd' and other attributes in chunk element ('i', 's', 'q') # # basically, write a proper implementation of manifest parsing and chunk # downloading import os import re import sys import xml.etree.ElementTree as etree import urllib2 import struct import tempfile from optparse import OptionParser from urlparse import urlparse, urlunparse __description__ = "Download videos served using Smooth Streaming technology" __version__ = "0.x" __author_info__ = "Written by Antonio Ospite http://ao2.it" def get_chunk_data(data): moof_size = struct.unpack(">L", data[0:4])[0] mdat_size = struct.unpack(">L", data[moof_size:moof_size + 4])[0] data_start = moof_size + 4 + len('mdat') data_size = mdat_size - 4 - len('mdat') # print len(data[data_start:]), \ # len(data[data_start:data_start + data_size]), data_size assert len(data[data_start:]) == data_size return data[data_start:data_start + data_size] def hexstring_to_bytes(hex_string): res = "" for i in range(0, len(hex_string), 2): res += chr(int(hex_string[i:i + 2], 16)) return res def write_wav_header(out_file, fmt, codec_private_data, data_len): extradata = hexstring_to_bytes(codec_private_data) fmt['cbSize'] = len(extradata) fmt_len = 18 + fmt['cbSize'] wave_len = len("WAVEfmt ") + 4 + fmt_len + len('data') + 4 out_file.write("RIFF") out_file.write(struct.pack(' if Duration is not available duration = manifest.getroot().attrib['Duration'] return float(duration) / 10000000 # here is the default timescale def smooth_download(url, manifest, dest_dir, video_stream_index=0, audio_stream_index=1, video_quality_level=0, audio_quality_level=0, chunks_dir=None, download=True, out_video_file='_video.vc1', out_audio_file='_audio.raw'): if chunks_dir is None: chunks_dir = dest_dir if download: download_chunks(url, manifest, video_stream_index, video_quality_level, chunks_dir) download_chunks(url, manifest, audio_stream_index, audio_quality_level, chunks_dir) dest_video = os.path.join(dest_dir, out_video_file) dest_audio = os.path.join(dest_dir, out_audio_file) rebuild_stream(manifest, video_stream_index, video_quality_level, chunks_dir, dest_video) rebuild_stream(manifest, audio_stream_index, audio_quality_level, chunks_dir, dest_audio, dest_audio + '.wav') # duration = get_clip_duration(manifest) delay = calc_tracks_delay(manifest, video_stream_index, audio_stream_index) # optionally encode audio to vorbis: # ffmpeg -i _audio.raw.wav -acodec libvorbis -aq 60 audio.ogg mux_command = ("ffmpeg -i %s \\\n" + " -itsoffset %f -async 1 -i %s \\\n" + " -vcodec copy -acodec copy ffout.mkv") % \ (dest_video, delay, dest_audio + '.wav') print mux_command def options_parser(): version = "%%prog %s" % __version__ usage = "usage: %prog [options] " parser = OptionParser(usage=usage, version=version, description=__description__, epilog=__author_info__) parser.add_option("-i", "--info", action="store_true", dest="info_only", default=False, help="print Manifest info and exit") parser.add_option("-m", "--manifest-only", action="store_true", dest="manifest_only", default=False, help="download Manifest file and exit") parser.add_option("-n", "--no-download", action="store_false", dest="download", default=True, help="disable downloading chunks") parser.add_option("-s", "--sync-delay", action="store_true", dest="sync_delay", default=False, help="show the sync delay between the given streams and exit") parser.add_option("-d", "--dest-dir", metavar="", dest="dest_dir", default=tempfile.gettempdir(), help="destination directory") parser.add_option("-c", "--chunks-dir", metavar="", dest="chunks_dir", default=None, help="directory containing chunks, if different from destination dir") parser.add_option("-v", "--video-stream", metavar="", type="int", dest="video_stream_index", default=0, help="index of the video stream") parser.add_option("-a", "--audio-stream", metavar="", type="int", dest="audio_stream_index", default=1, help="index of the audio stream") parser.add_option("-q", "--video-quality", metavar="", type="int", dest="video_quality_level", default=0, help="index of the video quality level") parser.add_option("-Q", "--audio-quality", metavar="", type="int", dest="audio_quality_level", default=0, help="index of the audio quality level") return parser def main(): parser = options_parser() (options, args) = parser.parse_args() if len(args) != 1: parser.print_help() parser.exit(1) if not os.path.exists(options.dest_dir): os.mkdir(options.dest_dir, 0755) url = args[0] manifest, url = get_manifest(url, options.dest_dir) if options.manifest_only: parser.exit(0) if options.sync_delay: print calc_tracks_delay(manifest, options.video_stream_index, options.audio_stream_index) parser.exit(0) if options.info_only: print_manifest_info(manifest) parser.exit(0) print_manifest_info(manifest) smooth_download(url, manifest, options.dest_dir, options.video_stream_index, options.audio_stream_index, options.video_quality_level, options.audio_quality_level, options.chunks_dir, options.download) if __name__ == "__main__": main()