From 4f04dbdd094f1fa6f4260ba83df1e6a0df08797f Mon Sep 17 00:00:00 2001 From: Gavin Beatty Date: Fri, 21 Dec 2012 01:38:53 +0000 Subject: [PATCH] package and setup.py improvements, and --audio-track and --video-track --- .gitignore | 6 +- MANIFEST | 14 ++ Makefile | 58 ++---- dist.mk | 41 ---- gen-version.mk | 26 --- gen-version.sh | 33 ---- mkvtomp4.py | 441 +----------------------------------------- setup.py | 94 ++++++--- simplemkv/__init__.py | 1 + simplemkv/info.py | 150 ++++++++++++++ simplemkv/tomp4.py | 387 ++++++++++++++++++++++++++++++++++++ 11 files changed, 635 insertions(+), 616 deletions(-) create mode 100644 MANIFEST delete mode 100644 dist.mk delete mode 100644 gen-version.mk delete mode 100755 gen-version.sh mode change 100644 => 100755 mkvtomp4.py create mode 100644 simplemkv/__init__.py create mode 100644 simplemkv/info.py create mode 100755 simplemkv/tomp4.py diff --git a/.gitignore b/.gitignore index 45b35f9..7dd6732 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ build -mkvtomp4-*-doc.zip -VERSION -mkvtomp4 -setup.py +dist +simplemkv/version.py diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..ed5e0b9 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,14 @@ +MANIFEST +setup.py +LICENSE +README.md +Makefile +man2txt.mk +mkvtomp4.py +simplemkv/__init__.py +simplemkv/version.py +simplemkv/info.py +simplemkv/tomp4.py +doc/mkvtomp4.1 +doc/mkvtomp4.1.html +doc/mkvtomp4.txt diff --git a/Makefile b/Makefile index 61fa9a3..eede541 100644 --- a/Makefile +++ b/Makefile @@ -1,38 +1,18 @@ -prefix=/usr/local TMPDIR=/tmp RM = rm -f A2X = a2x ASCIIDOC = asciidoc INSTALL = install -SED = sed -RSYNC = rsync -default: all -.PHONY: all +default: doc +.PHONY: default PROJECT = mkvtomp4 -include gen-version.mk -include dist.mk include man2txt.mk -all: bin doc -bin: $(PROJECT) doc: doc/$(PROJECT).1 doc/$(PROJECT).1.html doc/$(PROJECT).txt -clean: clean-bin clean-doc -.PHONY: all bin doc clean - -$(PROJECT): $(PROJECT).py $(VERSION_DEP) - $(SED) -e "s/^__version__ = .*/__version__ = '$(VERSION)'/" \ - $(PROJECT).py > $(PROJECT) - @chmod +x $(PROJECT) -clean-bin: - $(RM) $(PROJECT) -install-bin: - @echo 'make install-doc to install documentation.' - @echo 'To install the script, see README.markdown.' -install: install-bin -.PHONY: clean-bin install install-bin +.PHONY: doc doc/$(PROJECT).1: doc/$(PROJECT).1.txt $(A2X) -f manpage -L doc/$(PROJECT).1.txt @@ -40,29 +20,19 @@ doc/$(PROJECT).1.html: doc/$(PROJECT).1.txt $(ASCIIDOC) doc/$(PROJECT).1.txt doc/$(PROJECT).txt: doc/$(PROJECT).1 $(call man2txt,doc/$(PROJECT).1,doc/$(PROJECT).txt) -clean-doc: +clean: $(RM) doc/$(PROJECT).1 doc/$(PROJECT).1.html doc/$(PROJECT).txt -install-doc: doc/$(PROJECT).1 doc/$(PROJECT).1.html - $(INSTALL) -d -m 0755 $(DESTDIR)$(prefix)/share/man/man1 - $(INSTALL) -m 0644 doc/$(PROJECT).1 $(DESTDIR)$(prefix)/share/man/man1 - $(INSTALL) -m 0644 doc/$(PROJECT).1.html $(DESTDIR)$(prefix)/share/man/man1 - $(INSTALL) -d -m 0755 $(DESTDIR)$(prefix)/share/doc/$(PROJECT) - $(INSTALL) -m 0644 README.markdown $(DESTDIR)$(prefix)/share/doc/$(PROJECT) - $(INSTALL) -m 0644 LICENSE $(DESTDIR)$(prefix)/share/doc/$(PROJECT) -easy_install_doc: doc/$(PROJECT).1 doc/$(PROJECT).1.html doc/$(PROJECT).1.txt - @$(RM) $(DISTNAME)-doc.zip - @mkdir -p $(DISTNAME)-doc - @$(INSTALL) -m 0644 doc/$(PROJECT).1.html $(DISTNAME)-doc/index.html - $(ZIP) $(DISTNAME)-doc.zip $(DISTNAME)-doc/index.html >/dev/null - @$(RM) -r $(DISTNAME)-doc -.PHONY: clean-doc install-doc easy_install_doc - -upload-html: doc/$(PROJECT).1.html - $(RSYNC) -av --chmod u=rw,g=r,o=r doc/$(PROJECT).1.html stokes:~/www/ -.PHONY: upload-html +.PHONY: clean +#easy_install_doc: doc/$(PROJECT).1 doc/$(PROJECT).1.html doc/$(PROJECT).1.txt +# @$(RM) $(DISTNAME)-doc.zip +# @mkdir -p $(DISTNAME)-doc +# @$(INSTALL) -m 0644 doc/$(PROJECT).1.html $(DISTNAME)-doc/index.html +# $(ZIP) $(DISTNAME)-doc.zip $(DISTNAME)-doc/index.html >/dev/null +# @$(RM) -r $(DISTNAME)-doc +#.PHONY: easy_install_doc pep8: - pep8 mkvtomp4.py + @find . -name '*.py' -print0 | xargs -0 pep8 pyflakes: - pyflakes mkvtomp4.py + @find . -name '*.py' -print0 | xargs -0 pyflakes .PHONY: pep8 pyflakes diff --git a/dist.mk b/dist.mk deleted file mode 100644 index 2b40b2c..0000000 --- a/dist.mk +++ /dev/null @@ -1,41 +0,0 @@ -ifndef GIT -GIT = git -endif -ifndef ZIP -ZIP = zip -endif -ifndef BZIP2 -BZIP2 = bzip2 -endif -ifndef TAR -TAR = tar -endif -ifndef RM -RM = rm -f -endif -ifndef PROJECT_VERSION_VAR -PROJECT_VERSION_VAR = VERSION -endif -ifndef PROJECT_VERSION_FILE -PROJECT_VERSION_FILE = VERSION -endif -ifndef PROJECT -$(error "Must define PROJECT for use with dist.mk") -endif -ifndef DISTNAME -DISTNAME = $(PROJECT)-$($(PROJECT_VERSION_VAR)) -endif - -VERSION_ = $($(PROJECT_VERSION_VAR)) -dist: all $(PROJECT_VERSION_FILE) - @mkdir -p $(DISTNAME) - @echo $(PROJECT_VERSION_VAR)=$(VERSION_) > $(DISTNAME)/release - $(GIT) archive --format zip --prefix=$(DISTNAME)/ \ - HEAD^{tree} --output $(DISTNAME).zip - @$(ZIP) -u $(DISTNAME).zip $(DISTNAME)/release >/dev/null - $(GIT) archive --format tar --prefix=$(DISTNAME)/ \ - HEAD^{tree} --output $(DISTNAME).tar - @$(TAR) rf $(DISTNAME).tar $(DISTNAME)/release - @$(RM) -r $(DISTNAME) - $(BZIP2) -9 $(DISTNAME).tar - diff --git a/gen-version.mk b/gen-version.mk deleted file mode 100644 index c5cbc03..0000000 --- a/gen-version.mk +++ /dev/null @@ -1,26 +0,0 @@ - -ifndef GIT -GIT = git -endif -ifndef RM -RM = rm -f -endif -ifndef PROJECT_VERSION_VAR -PROJECT_VERSION_VAR = VERSION -endif -ifndef PROJECT_RELEASE_FILE -PROJECT_RELEASE_FILE = release -endif - --include $(PROJECT_RELEASE_FILE) -ifeq ($(strip $($(PROJECT_VERSION_VAR))),) -$(PROJECT_VERSION_VAR)_DEP=$(PROJECT_VERSION_VAR) -$(PROJECT_VERSION_VAR): ./gen-version.sh .git/$(shell $(GIT) symbolic-ref HEAD) - @./gen-version.sh $(PROJECT) $(PROJECT_VERSION_VAR) $(PROJECT_VERSION_VAR) --include $(PROJECT_VERSION_VAR) -else -$(PROJECT_VERSION_VAR)_DEP= -endif -clean-version: - $(RM) $(PROJECT_VERSION_VAR) -clean: clean-version diff --git a/gen-version.sh b/gen-version.sh deleted file mode 100755 index 7f3482a..0000000 --- a/gen-version.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh - -set -e -set -u - -progname="$1" -vvar="$2" -vfile="$3" -shift 3 - -lf=' -' - -if test -f release ; then - cp release "$vfile" - cat "$vfile" >&2 -else - ver="$(git describe "$@" --abbrev=4 | sed -e "s/^${progname}-v//")" - case "$ver" in - "$lf") - exit 1 - ;; - [0-9]*) - git update-index -q --refresh - if test -n "$(git diff-index --name-only HEAD --)" ; then - ver="${ver}-dirty" - fi - ;; - esac - echo "${vvar} = $ver" >&2 - echo "${vvar}=${ver}" > "$vfile" -fi - diff --git a/mkvtomp4.py b/mkvtomp4.py old mode 100644 new mode 100755 index 44c588f..2113813 --- a/mkvtomp4.py +++ b/mkvtomp4.py @@ -1,444 +1,5 @@ #!/usr/bin/python - -"""Convert H.264 mkv files to mp4 files playable on the PS3, and "correct" the -MPEG4/ISO/AVC profile for use on the PS3.""" - - -__version__ = '1.3' - - -usage = 'usage: mkvtomp4 [options] [--] ' - - -import os import sys -import re -import subprocess as sp -import struct -import getopt -import pipes -try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO - - -def prin(*args, **kwargs): - fobj = kwargs.get('fobj', None) - if fobj is None: - fobj = sys.stdout - sep = kwargs.get('sep', ' ') - end = kwargs.get('end', '\n') - if len(args) > 0: - fobj.write(args[0]) - if len(args) > 1: - for arg in args[1:]: - fobj.write(sep + arg) - fobj.write(end) - - -def eprint(*args, **kwargs): - kwargs['fobj'] = sys.stderr - prin("error:", *args, **kwargs) - - -def die(*args, **kwargs): - eprint(*args, **kwargs) - sys.exit(1) - - -def wprint(*args, **kwargs): - kwargs['fobj'] = sys.stderr - prin("warning:", *args, **kwargs) - - -_verbosity = 0 - - -def vprint(level, *args, **kwargs): - global _verbosity - local = kwargs.get('verbosity', 0) - if _verbosity >= level or local >= level: - prin('verbose:', *args, **kwargs) - - -def onlykeys(d, keys): - newd = {} - for k in keys: - newd[k] = d[k] - return newd - - -def __sq(one): - squoted = pipes.quote(one) - if squoted == '': - return "''" - return squoted - - -def sq(args): - return " ".join([__sq(x) for x in args]) - - -def command(cmd, **kwargs): - verbose_kwargs = {} - verbosity = kwargs.get('verbosity', None) - if verbosity is not None: - verbose_kwargs['verbosity'] = verbosity - if len(kwargs) != 0: - vprint(1, 'command: Popen kwargs: %s' % str(kwargs), **verbose_kwargs) - try: - vprint(1, 'command: %s' % str(cmd), **verbose_kwargs) - proc = sp.Popen( - cmd, stdout=sp.PIPE, stderr=sp.PIPE, close_fds=True, **kwargs - ) - except OSError, e: - die('command failed:', str(e), ':', sq(cmd)) - chout, cherr = proc.communicate() - vprint(1, 'command: stdout:', chout, '\ncommand: stderr:', cherr) - if proc.returncode != 0: - die('failure: %s' % cherr, end='') - return chout - - -def dry_command(cmd, **opts): - if opts['dry_run']: - prin(sq(cmd)) - else: - command(cmd, **opts) - - -def dry_system(cmd, **opts): - quoted = sq(cmd) - if opts['dry_run']: - prin(quoted) - else: - os.system(quoted) - - -class MkvInfo(object): - _impl = None - - class _Impl: - def __init__(self): - self.track_no_re = re.compile(r'^\| \+ Track number: (\d+)$') - self.track_type_re = re.compile(r'^\| \+ Track type: (.*)$') - self.codec_re = re.compile(r'^\| \+ Codec ID: (.*)$') - self.a_codec_re = re.compile(r'^(A_)?(DTS|AAC|AC3)$') - self.v_codec_re = re.compile(r'^(V_)?(MPEG4/ISO/AVC)$') - self.fps_re = re.compile( - r'^\| \+ Default duration: \d+\.\d+ms \((\d+\.\d+)' - ' fps for a video track\)$') - - @classmethod - def info(cls, mkv): - if cls._impl is None: - cls._impl = cls._Impl() - i = {} - i['track'] = {'audio': None, 'video': None} - i['fullinfo'] = command( - ['mkvinfo', mkv], env={'LC_ALL': 'C'} - ).split('\n') - in_track_number = None - in_track_type = None - for line in i['fullinfo']: - match = cls._impl.track_no_re.search(line) - if match is not None: - in_track_number = match.group(1) - in_track_type = None - vprint(1, 'MkvInfo: in track number: %s' % in_track_number) - continue - if in_track_number is not None: - match = cls._impl.track_type_re.search(line) - if match is not None: - in_track_type = match.group(1) - vprint(1, 'MkvInfo: in track type: %s' % in_track_type) - i['track'][in_track_type] = in_track_number - if in_track_type != 'audio' and in_track_type != 'video': - wprint('ignoring track type: %s' % in_track_type) - if in_track_number is not None: - match = cls._impl.codec_re.search(line) - if match is not None: - codec = match.group(1) - # unknown track types shouldn't have codec_match := None - codec_match = None - if in_track_type == 'audio': - codec_match = cls._impl.a_codec_re.search(codec) - if codec_match is None: - die('unrecognised codec: %s' % codec) - elif in_track_type == 'video': - codec_match = cls._impl.v_codec_re.search(codec) - if codec_match is None: - die('unrecognised codec: %s' % codec) - if codec_match is not None: - key = in_track_type + '_codec' - i['track'][key] = codec_match.group(2) - vprint(1, 'MkvInfo: found %s: %s' - % (key, i['track'][key])) - if in_track_type == 'video': - match = cls._impl.fps_re.search(line) - if match is not None: - i['track']['fps'] = match.group(1) - - -def default_options(argv0): - return { - 'argv0': argv0, - 'verbosity': 0, - 'a_bitrate': '328', - 'a_channels': '5.1', - 'a_codec': 'libfaac', - 'a_delay': None, - 'output': None, - 'keep_temp_files': False, - 'dry_run': False, - 'correct_prof_only': False, - 'stop_v_ex': False, - 'stop_correct': False, - 'stop_a_ex': False, - 'stop_a_conv': False, - 'stop_v_mp4': False, - 'stop_hint_mp4': False, - 'stop_a_mp4': False, - 'mp4': 'mp4creator', - } - - -def mp4_add_audio_optimize_cmd(mp4, audio, **kwargs): - if kwargs['mp4'] == 'mp4creator': - return ['mp4creator', '-c', audio, '-interleave', '-optimize', mp4] - elif kwargs['mp4'] == 'mp4box': - delay = kwargs.get('delay', None) - if delay is not None: - delay = ':delay=' + delay - else: - delay = '' - return ['MP4Box', '-add', audio + '#audio:trackID=2' + delay, mp4] - - -def mp4_add_hint_cmd(mp4, **kwargs): - if kwargs['mp4'] == 'mp4creator': - return ['mp4creator', '-hint=1', mp4] - elif kwargs['mp4'] == 'mp4box': - return None - - -def mp4_add_video_cmd(mp4, video, fps, **kwargs): - if kwargs['mp4'] == 'mp4creator': - return ['mp4creator', '-c', video, '-rate', fps, mp4] - elif kwargs['mp4'] == 'mp4box': - return [ - 'MP4Box', '-add', - video + '#video:trackID=1', '-hint', '-fps', fps, mp4, - ] - - -def ffmpeg_convert_audio_cmd(old, new, **kwargs): - bitrate = kwargs.get('bitrate', '128') - channels = kwargs.get('channels', '2') - codec = kwargs.get('codec', 'libfaac') - verbosity = kwargs.get('verbosity', 0) - if str(channels) == '5.1': - channels = '6' - if verbosity > 1: - cmd = ['ffmpeg', '-v', str(verbosity - 1)] - else: - cmd = ['ffmpeg'] - return cmd + [ - '-i', old, '-ac', str(channels), '-acodec', codec, - '-ab', str(bitrate) + 'k', new - ] - - -def pretend_correct_rawmp4_profile(rawmp4, argv0): - prin(sq([argv0, '--correct-profile-only', rawmp4])) - - -def correct_rawmp4_profile(rawmp4): - level_string = struct.pack('b', int('29', 16)) - f = open(rawmp4, 'r+b') - try: - f.seek(7) - vprint(1, 'correcting profile:', rawmp4) - f.write(level_string) - finally: - f.close() - - -def dry_correct_rawmp4_profile(rawmp4, **opts): - if opts['dry_run']: - pretend_correct_rawmp4_profile(rawmp4, opts['argv0']) - else: - correct_rawmp4_profile(rawmp4, **opts) - - -def mkv_extract_track_cmd(mkv, out, track, verbosely=False): - v = ['-v'] if verbosely else [] - return ['mkvextract', 'tracks', mkv] + v + [str(track) + ':' + out] - - -# XXX not used -#def mkv_split(mkv, pieces): -# if pieces != 1: -# split_size_MB = (((os.path.getsize(mkv) / pieces) + 1) / 1000) + 1 -# command(['mkvmerge', '-o', mkv, '--split', str(split_size_MB)]) - - -def exit_if(bbool, value=0): - if bbool: - sys.exit(value) - - -def real_main(mkv, **opts): - mkvinfo = MkvInfo.info(mkv) - mkvtracks = mkvinfo['track'] - if mkvtracks.get('video', None) is None: - die('no video track found in info:\n' + mkvinfo['fullinfo']) - if mkvtracks.get('audio', None) is None: - die('no audio track found in info:\n' + mkvinfo['fullinfo']) - tempfiles = [] - try: - # Extract video - video = mkv + '.h264' - exit_if(opts['stop_v_ex']) - extract_cmd = mkv_extract_track_cmd( - mkv, out=video, track=mkvtracks['video'], - verbosely=(opts['verbosity'] > 0), - ) - tempfiles.append(video) - dry_command(extract_cmd, **opts) - exit_if(opts['stop_correct']) - # Correct profile - dry_correct_rawmp4_profile(video, **opts) - a_codec = mkvtracks['audio_codec'] - audio = mkv + '.' + a_codec.lower() - exit_if(opts['stop_a_ex']) - # Extract audio - extract_cmd = mkv_extract_track_cmd( - mkv, out=audio, track=mkvtracks['audio'], - verbosely=(opts['verbosity'] > 0) - ) - tempfiles.append(audio) - dry_command(extract_cmd, **opts) - exit_if(opts['stop_a_conv']) - # Convert audio - if str(a_codec).lower() != 'aac': - aacaudio, oldaudio = audio + '.aac', audio - audio_cmd = ffmpeg_convert_audio_cmd(oldaudio, aacaudio, **opts) - tempfiles.append(aacaudio) - dry_system(audio_cmd, **opts) - if opts['output'] is None: - opts['output'] = os.path.splitext(mkv)[0] + '.mp4' - exit_if(opts['stop_v_mp4']) - # Create mp4 container with video - mp4video_cmd = mp4_add_video_cmd( - opts['output'], video, - fps=mkvtracks['fps'] - ) - dry_command(mp4video_cmd, **opts) - exit_if(opts['stop_hint_mp4']) - # Hint mp4 container - mp4hint_cmd = mp4_add_hint_cmd(opts['output'], **opts) - dry_command(mp4hint_cmd, **opts) - exit_if(opts['stop_a_mp4']) - # Add audio to mp4 container and optimize - mp4opt_cmd = mp4_add_audio_optimize_cmd( - opts['output'], aacaudio, - **opts - ) - dry_command(mp4opt_cmd, **opts) - finally: - if not opts['keep_temp_files']: - for f in tempfiles: - try: - os.remove(f) - except OSError: - pass - - -def parseopts(argv=None): - opts = default_options(argv[0]) - try: - options, arguments = getopt.gnu_getopt( - argv[1:], - 'hvo:n', - [ - 'help', 'usage', 'version', 'verbose', - 'use-mp4box', 'use-mp4creator', - 'audio-delay-ms=', 'audio-bitrate=', 'audio-channels=', - 'audio-codec=', - 'output=', 'keep-temp-files', 'dry-run', - 'correct-profile-only', - 'stop-before-extract-video', 'stop-before-correct-profile', - 'stop-before-extract-audio', 'stop-before-convert-audio', - 'stop-before-video-mp4', 'stop-before-hinting-mp4', - 'stop-before-audio-mp4', - ] - ) - except getopt.GetoptError, err: - die(str(err)) - for opt, optarg in options: - if opt in ('-h', '--help', '--usage'): - prin(usage) - sys.exit(0) - elif opt == '--version': - prin(__version__) - sys.exit(0) - elif opt in ('-v', '--verbose'): - opts['verbosity'] = opts['verbosity'] + 1 - elif opt == '--use-mp4creator': - opts['mp4'] = 'mp4creator' - elif opt == '--use-mp4box': - opts['mp4'] = 'mp4box' - elif opt == '--audio-delay-ms': - opts['a_delay'] = optarg - elif opt == '--audio-bitrate': - opts['a_bitrate'] = optarg - elif opt == '--audio-channels': - opts['a_channels'] = optarg - elif opt == '--audio-codec': - opts['a_codec'] = optarg - elif opt in ('-o', '--output'): - opts['output'] = optarg - elif opt == '--keep-temp-files': - opts['keep_temp_files'] = True - elif opt in ('-n', '--dry-run'): - opts['dry_run'] = True - elif opt == '--correct-profile-only': - opts['correct_prof_only'] = True - elif opt == '--stop-before-extract-video': - opts['stop_v_ex'] = True - elif opt == '--stop-before-correct-profile': - opts['stop_correct'] = True - elif opt == '--stop-before-extract-audio': - opts['stop_a_ex'] = True - elif opt == '--stop-before-convert-audio': - opts['stop_a_conv'] = True - elif opt == '--stop-before-video-mp4': - opts['stop_v_mp4'] = True - elif opt == '--stop-before-hinting-mp4': - opts['stop_hint_mp4'] = True - elif opt == '--stop-before-audio-mp4': - opts['stop_a_mp4'] = True - return opts, arguments - - -def main(argv=None): - if argv is None: - argv = sys.argv - opts, args = parseopts(argv) - if len(args) != 1: - die(usage) - if opts['a_delay'] is not None and opts['mp4'] == 'mp4creator': - die("Cannot use --audio-delay-ms with mp4creator. Try --use-mp4box") - try: - if opts['correct_prof_only']: - dry_correct_rawmp4_profile(args[0], **opts) - else: - real_main(args[0], **opts) - except Exception, e: - die('failed with exception:', str(e)) - - +from simplemkv.tomp4 import main if __name__ == "__main__": sys.exit(main()) diff --git a/setup.py b/setup.py index 2bfc10f..cffd675 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ - -"""Convert H.264 mkv files to mp4 files playable on the PS3 - -Uses mpeg4ip, mkvtoolnix and ffmpeg to convert troublesome mkv files to mp4. -They will be playable on the Sony PS3. -""" - -from distutils.core import setup -import mkvtomp4 +import sys +import os +import subprocess as sp +from distutils.core import setup, Command +from simplemkv.tomp4 import __doc__ +try: + from simplemkv.version import __version__ +except ImportError: + __version__ = 'unknown' # A list of classifiers can be found here: # http://pypi.python.org/pypi?%3Aaction=list_classifiers @@ -23,9 +23,45 @@ Operating System :: OS Independent """ -from sys import version_info +def write_version(v): + f = open('simplemkv/version.py', 'w') + try: + f.write('__version__ = %s\n' % repr(v)) + finally: + f.close() + +def git_version(): + cmd = ['git', 'describe', '--abbrev=4'] + try: + proc = sp.Popen(cmd, stdout=sp.PIPE) + stdout = proc.communicate()[0].rstrip('\n') + except OSError: + sys.stderr.write('git not found: leaving __version__ alone\n') + return __version__ + if proc.returncode != 0: + sys.stderr.write('git describe failed: leaving __version__ alone\n') + return __version__ + ver = stdout.lstrip('mkvtomp4-v') + write_version(ver) + try: + proc = sp.Popen(['git', 'update-index', '-q', '--refresh']) + proc.communicate() + except OSError: + return ver + if proc.returncode != 0: + return ver + try: + proc = sp.Popen(['git', 'diff-index', '--name-only', 'HEAD', '--'], stdout=sp.PIPE) + stdout = proc.communicate()[0] + except OSError: + sys.stderr.write('git diff-index failed\n') + if stdout.strip('\n'): + ver = ver + '-dirty' + write_version(ver) + return ver +__version__ = git_version() -if version_info < (2, 3): +if sys.version_info < (2, 3): _setup = setup def setup(**kwargs): if kwargs.has_key("classifiers"): @@ -34,21 +70,23 @@ def setup(**kwargs): doclines = __doc__.split("\n") -setup(name='mkvtomp4' - ,description=doclines[0] - ,long_description="\n".join(doclines[2:]) - ,author='Gavin Beatty' - ,author_email='gavinbeatty@gmail.com' - ,maintainer='Gavin Beatty' - ,maintainer_email='gavinbeatty@gmail.com' - ,license = 'http://opensource.org/licenses/MIT' - ,platforms=["any"] - ,classifiers=filter(None, classifiers.split("\n")) - ,url='http://code.google.com/p/mkvtomp4/' - ,version=mkvtomp4.__version__ - ,scripts=['mkvtomp4'] -# ,data_files=[('share/doc/mkvtomp4', ['README.md']) -# , ('share/man/man1', ['doc/mkvtomp4.1', 'doc/mkvtomp4.1.html']) -# ] - ) +setup(name='mkvtomp4', + description=doclines[0], + long_description="\n".join(doclines[2:]), + author='Gavin Beatty', + author_email='gavinbeatty@gmail.com', + maintainer='Gavin Beatty', + maintainer_email='gavinbeatty@gmail.com', + license = 'http://opensource.org/licenses/MIT', + platforms=["any"], + classifiers=filter(None, classifiers.split("\n")), + url='http://code.google.com/p/mkvtomp4/', + version=__version__, + scripts=['mkvtomp4.py'], + py_modules=['simplemkv.info', 'simplemkv.tomp4'], + data_files=[ + ('share/doc/mkvtomp4', ['README.md', 'doc/mkvtomp4.txt']), + ('share/man/man1', ['doc/mkvtomp4.1', 'doc/mkvtomp4.1.html']), + ], +) diff --git a/simplemkv/__init__.py b/simplemkv/__init__.py new file mode 100644 index 0000000..2f37aab --- /dev/null +++ b/simplemkv/__init__.py @@ -0,0 +1 @@ +__all__ = ['info', 'tomp4'] diff --git a/simplemkv/info.py b/simplemkv/info.py new file mode 100644 index 0000000..e6077bf --- /dev/null +++ b/simplemkv/info.py @@ -0,0 +1,150 @@ +import sys +import os +import re +import subprocess as sp + +try: + from .version import __version__ +except ImportError: + __version__ = 'unknown' + +def indent_level(line): + """Get the indent level for a line of mkvinfo output. + Returns -1 if *line* is not the correct format.""" + m = re.search(r'^\|( *)\+', line) + if not m: + return -1 + return len(m.group(1)) + + +class TrackLineHandler: + 'Parse a line of (English) mkvinfo output inside "A track".' + _number = '| + Track number: ' + _type = '| + Track type: ' + _codec = '| + Codec ID: ' + _lang = '| + Language: ' + _duration = '| + Default duration: ' + _fps_re = re.compile(r'\((.*?) frames/fields per second for a video track\)') + + def __init__(self, infodict): + self._info = infodict + + def _findvalue(self, key, s): + idx = s.find(key) + if idx != -1: + return s[idx + len(key):] + return None + + def line(self, handlers, l): + self._track = self._info['tracks'][-1] + cls = TrackLineHandler + ind = indent_level(l) + if ind == -1 or ind < 2: + handlers.pop(-1) + return False + number = self._findvalue(cls._number, l) + if number: + endidx = number.find(' ') + if endidx == -1: + number = int(number) + else: + number = int(number[:endidx]) + self._track['number'] = number - 1 + return True + typ = self._findvalue(cls._type, l) + if typ: + self._track['type'] = typ + return True + codec = self._findvalue(cls._codec, l) + if codec: + self._track['codec'] = codec + return True + lang = self._findvalue(cls._lang, l) + if lang: + self._track['language'] = lang + return True + if self._track.get('type', '') == 'video': + duration = self._findvalue(cls._duration, l) + if duration: + match = cls._fps_re.search(l) + if match: + self._track['fps'] = float(match.group(1)) + return True + return True + + +class MainLineHandler: + "Parse a line of (locale='en_US') mkvinfo output." + def __init__(self, infodict): + self._info = infodict + self._track = TrackLineHandler(infodict) + + def line(self, handlers, l): + if l.startswith('|+ Segment tracks'): + self._info.setdefault('tracks', []) + return True + elif l.startswith('| + A track'): + self._info['tracks'].append({}) + handlers.append(self._track) + return True + return True + + +def info_locale_opts(locale): + """Example usage with *infostring*:: + + opts = info_locale_opts('en_US') + opts.setdefault('arguments', []) + opts['arguments'].extend(['-x', '-r', 'mkvinfo.log']) + opts.setdefault('env', {}) + opts['env']['MTX_DEBUG'] = 'topic' + print infostring(mkv, **opts) + """ + return {'arguments': ['--ui-language', locale]} + + +def infostring(mkv, env=None, arguments=[], errorfunc=sys.exit): + """Run mkvinfo on the given *mkv* and returns stdout as a single string. + + On failure, calls *errorfunc* with an error string. + + It's likely you'll want to set *env* or *arguments* to use ``'en_US'`` + locale, since that is what *infodict* requires. See + *info_locale_opts*. + """ + cmd = ['mkvinfo'] + arguments + [mkv] + opts = {} + if env is not None: + env.setdefault('PATH', os.environ.get('PATH', '')) + env.setdefault('SystemRoot', os.environ.get('SystemRoot', '')) + opts = {'env': env} + proc = sp.Popen( + cmd, stdout=sp.PIPE, stderr=sp.PIPE, close_fds=True, **opts + ) + out, err = proc.communicate() + if proc.returncode != 0: + errorfunc('command failed: ' + err.rstrip('\n')) + return out + + +def infodict(lines): + """Take a list of *lines* of ``locale='en_US'`` mkvinfo output and return a + dictionary of info.""" + inf = {'lines': lines} + handlers = [MainLineHandler(inf)] + for l in lines: + while not handlers[-1].line(handlers, l): + if not handlers: + break + if not handlers: + break + return inf + + +if __name__ == '__main__': + from pprint import pprint + mkv = sys.argv[1] + s = infostring(mkv, arguments=['--ui-language', 'en_US']) + d = infodict(inf.rstrip('\n').split('\n')) + del d['lines'] + pprint(d) diff --git a/simplemkv/tomp4.py b/simplemkv/tomp4.py new file mode 100755 index 0000000..b972882 --- /dev/null +++ b/simplemkv/tomp4.py @@ -0,0 +1,387 @@ +"""Convert H.264 mkv files to mp4 files playable on the PS3, and "correct" the +MPEG4/ISO/AVC profile for use on the PS3.""" + +try: + from .version import __version__ +except ImportError: + __version__ = 'unknown' +from . import info + +import sys +import os +import re +import getopt +import subprocess as sp +import struct +import pipes + +import simplemkv.info + +usage = 'usage: mkvtomp4 [options] [--] ' + +def exit_if(bbool, value=0): + if bbool: + sys.exit(value) + + +def prin(*args, **kwargs): + fobj = kwargs.get('fobj', None) + if fobj is None: + fobj = sys.stdout + sep = kwargs.get('sep', ' ') + end = kwargs.get('end', '\n') + if len(args) > 0: + fobj.write(args[0]) + if len(args) > 1: + for arg in args[1:]: + fobj.write(sep + arg) + fobj.write(end) + + +def eprint(*args, **kwargs): + kwargs['fobj'] = sys.stderr + prin("error:", *args, **kwargs) + + +def die(*args, **kwargs): + eprint(*args, **kwargs) + sys.exit(1) + + +def wprint(*args, **kwargs): + kwargs['fobj'] = sys.stderr + prin("warning:", *args, **kwargs) + + +_verbosity = 0 + + +def vprint(level, *args, **kwargs): + global _verbosity + local = kwargs.get('verbosity', 0) + if _verbosity >= level or local >= level: + prin('verbose:', *args, **kwargs) + + +def onlykeys(d, keys): + newd = {} + for k in keys: + newd[k] = d[k] + return newd + + +def __sq(one): + if one == '': + return "''" + return pipes.quote(str(one)) + + +def sq(args): + return " ".join([__sq(x) for x in args]) + + +def command(cmd, **kwargs): + verbose_kwargs = {} + verbosity = kwargs.get('verbosity', None) + if verbosity is not None: + verbose_kwargs['verbosity'] = verbosity + if len(kwargs) != 0: + vprint(1, 'command: Popen kwargs: %s' % str(kwargs), **verbose_kwargs) + try: + vprint(1, 'command: %s' % str(cmd), **verbose_kwargs) + spopts = kwargs.get('spopts', {}) + proc = sp.Popen( + cmd, stdout=sp.PIPE, stderr=sp.PIPE, close_fds=True, **spopts + ) + except OSError, e: + die('command failed:', str(e), ':', sq(cmd)) + chout, cherr = proc.communicate() + vprint(1, 'command: stdout:', chout, '\ncommand: stderr:', cherr) + if proc.returncode != 0: + die('failure: %s' % cherr, end='') + return chout + + +def dry_command(cmd, **opts): + if opts['dry_run']: + prin(sq(cmd)) + else: + command(cmd, **opts) + + +def dry_system(cmd, **opts): + quoted = sq(cmd) + if opts['dry_run']: + prin(quoted) + else: + os.system(quoted) + + +def default_options(argv0): + return { + 'argv0': argv0, + 'verbosity': 0, + 'a_bitrate': '328', + 'a_channels': '5.1', + 'a_codec': 'libfaac', + 'a_delay': None, + 'output': None, + 'video_track': None, + 'audio_track': None, + 'keep_temp_files': False, + 'dry_run': False, + 'correct_prof_only': False, + 'stop_v_ex': False, + 'stop_correct': False, + 'stop_a_ex': False, + 'stop_a_conv': False, + 'stop_v_mp4': False, + 'stop_hint_mp4': False, + 'stop_a_mp4': False, + 'mp4': 'mp4creator', + } + + +def mp4_add_audio_optimize_cmd(mp4file, audio, **opts): + if opts['mp4'] == 'mp4creator': + return ['mp4creator', '-c', audio, '-interleave', '-optimize', mp4file] + elif opts['mp4'] == 'mp4box': + delay = opts.get('delay', None) + if delay is not None: + delay = ':delay=' + delay + else: + delay = '' + return ['MP4Box', '-add', audio + '#audio:trackID=2' + delay, mp4file] + + +def mp4_add_hint_cmd(mp4file, **opts): + if opts['mp4'] == 'mp4creator': + return ['mp4creator', '-hint=1', mp4file] + elif opts['mp4'] == 'mp4box': + return None + + +def mp4_add_video_cmd(mp4file, video, fps, **opts): + if opts['mp4'] == 'mp4creator': + return ['mp4creator', '-c', video, '-rate', fps, mp4file] + elif opts['mp4'] == 'mp4box': + return [ + 'MP4Box', '-add', + video + '#video:trackID=1', '-hint', '-fps', fps, mp4file, + ] + + +def ffmpeg_convert_audio_cmd(old, new, **opts): + bitrate = opts.get('bitrate', '128') + channels = opts.get('channels', '2') + codec = opts.get('codec', 'libfaac') + verbosity = opts.get('verbosity', 0) + if str(channels) == '5.1': + channels = '6' + if verbosity > 1: + cmd = ['ffmpeg', '-v', str(verbosity - 1)] + else: + cmd = ['ffmpeg'] + return cmd + [ + '-i', old, '-ac', str(channels), '-acodec', codec, + '-ab', str(bitrate) + 'k', new + ] + + +def pretend_correct_rawmp4_profile(rawmp4, argv0): + prin(sq([argv0, '--correct-profile-only', rawmp4])) + + +def correct_rawmp4_profile(rawmp4): + level_string = struct.pack('b', int('29', 16)) + f = open(rawmp4, 'r+b') + try: + f.seek(7) + vprint(1, 'correcting profile:', rawmp4) + f.write(level_string) + finally: + f.close() + + +def dry_correct_rawmp4_profile(rawmp4, **opts): + if opts['dry_run']: + pretend_correct_rawmp4_profile(rawmp4, opts['argv0']) + else: + correct_rawmp4_profile(rawmp4) + + +def mkv_extract_track_cmd(mkv, out, track, verbosely=False): + v = ['-v'] if verbosely else [] + return ['mkvextract', 'tracks', mkv] + v + [str(track) + ':' + out] + + +def real_main(mkvfile, **opts): + infostr = simplemkv.info.infostring(mkvfile, arguments=['--ui-language', 'en_US']) + info = simplemkv.info.infodict(infostr.split('\n')) + tracks = info['tracks'] + def get_track(typ, codec_re): + number = opts.get(typ + '_track', None) + if number is not None: + try: + track = tracks[number] + except IndexError: + die('track %d not found: %s' % (number, str(tracks))) + if not codec_re.search(track['codec']): + die('track %d has incorrect codec: %s' % (number, str(track))) + else: + types = [ + t for t in tracks + if t['type'] == typ # and codec_re.search(t['codec']) + ] + if not types: + die('appropriate %s track not found: %s' % (typ, str(tracks))) + return types[0] + videotrack = get_track('video', re.compile(r'^(?!V_)?MPEG4/ISO/AVC\b')) + audiotrack = get_track('audio', re.compile(r'^(?!A_)?(?!DTS|AAC|AC3)\b')) + tempfiles = [] + try: + # Extract video + video = mkvfile + '.h264' + exit_if(opts['stop_v_ex']) + extract_cmd = mkv_extract_track_cmd( + mkvfile, out=video, track=videotrack['number'], + verbosely=(opts['verbosity'] > 0), + ) + tempfiles.append(video) + dry_command(extract_cmd, **opts) + exit_if(opts['stop_correct']) + # Correct profile + dry_correct_rawmp4_profile(video, **opts) + a_codec = audiotrack['codec'] + audio = mkvfile + '.' + a_codec.lower() + exit_if(opts['stop_a_ex']) + # Extract audio + extract_cmd = mkv_extract_track_cmd( + mkvfile, out=audio, track=audiotrack['number'], + verbosely=(opts['verbosity'] > 0) + ) + tempfiles.append(audio) + dry_command(extract_cmd, **opts) + exit_if(opts['stop_a_conv']) + # Convert audio + if str(a_codec).lower() != 'aac': + aacaudio, oldaudio = audio + '.aac', audio + audio_cmd = ffmpeg_convert_audio_cmd(oldaudio, aacaudio, **opts) + tempfiles.append(aacaudio) + dry_system(audio_cmd, **opts) + if opts['output'] is None: + opts['output'] = os.path.splitext(mkvfile)[0] + '.mp4' + exit_if(opts['stop_v_mp4']) + # Create mp4 container with video + opts['fps'] = videotrack['fps'] + mp4video_cmd = mp4_add_video_cmd( + opts['output'], video, + **opts + ) + dry_command(mp4video_cmd, **opts) + exit_if(opts['stop_hint_mp4']) + # Hint mp4 container + mp4hint_cmd = mp4_add_hint_cmd(opts['output'], **opts) + dry_command(mp4hint_cmd, **opts) + exit_if(opts['stop_a_mp4']) + # Add audio to mp4 container and optimize + mp4opt_cmd = mp4_add_audio_optimize_cmd( + opts['output'], aacaudio, + **opts + ) + dry_command(mp4opt_cmd, **opts) + finally: + if opts['dry_run']: + prin(sq(['rm', '-f'] + tempfiles)) + elif not opts['keep_temp_files']: + for f in tempfiles: + try: + os.remove(f) + except OSError: + pass + + +def parseopts(argv=None): + opts = default_options(argv[0]) + try: + options, arguments = getopt.gnu_getopt( + argv[1:], + 'hvo:n', + [ + 'help', 'usage', 'version', 'verbose', + 'use-mp4box', 'use-mp4creator', + 'video-track=', 'audio-track=', + 'audio-delay-ms=', 'audio-bitrate=', 'audio-channels=', + 'audio-codec=', + 'output=', 'keep-temp-files', 'dry-run', + 'correct-profile-only', + 'stop-before-extract-video', 'stop-before-correct-profile', + 'stop-before-extract-audio', 'stop-before-convert-audio', + 'stop-before-video-mp4', 'stop-before-hinting-mp4', + 'stop-before-audio-mp4', + ] + ) + except getopt.GetoptError, err: + die(str(err)) + for opt, optarg in options: + if opt in ('-h', '--help', '--usage'): + prin(usage) + sys.exit(0) + elif opt == '--version': + prin(__version__) + sys.exit(0) + elif opt in ('-v', '--verbose'): + opts['verbosity'] = opts['verbosity'] + 1 + elif opt == '--use-mp4creator': + opts['mp4'] = 'mp4creator' + elif opt == '--use-mp4box': + opts['mp4'] = 'mp4box' + elif opt == '--video-track': + opts['video_track'] = optarg + elif opt == '--audio-track': + opts['audio_track'] = optarg + elif opt == '--audio-delay-ms': + opts['a_delay'] = optarg + elif opt == '--audio-bitrate': + opts['a_bitrate'] = optarg + elif opt == '--audio-channels': + opts['a_channels'] = optarg + elif opt == '--audio-codec': + opts['a_codec'] = optarg + elif opt in ('-o', '--output'): + opts['output'] = optarg + elif opt == '--keep-temp-files': + opts['keep_temp_files'] = True + elif opt in ('-n', '--dry-run'): + opts['dry_run'] = True + elif opt == '--correct-profile-only': + opts['correct_prof_only'] = True + elif opt == '--stop-before-extract-video': + opts['stop_v_ex'] = True + elif opt == '--stop-before-correct-profile': + opts['stop_correct'] = True + elif opt == '--stop-before-extract-audio': + opts['stop_a_ex'] = True + elif opt == '--stop-before-convert-audio': + opts['stop_a_conv'] = True + elif opt == '--stop-before-video-mp4': + opts['stop_v_mp4'] = True + elif opt == '--stop-before-hinting-mp4': + opts['stop_hint_mp4'] = True + elif opt == '--stop-before-audio-mp4': + opts['stop_a_mp4'] = True + return opts, arguments + + +def main(argv=None): + if argv is None: + argv = sys.argv + opts, args = parseopts(argv) + if len(args) != 1: + die(usage) + if opts['a_delay'] is not None and opts['mp4'] == 'mp4creator': + die("Cannot use --audio-delay-ms with mp4creator. Try --use-mp4box") + if opts['correct_prof_only']: + dry_correct_rawmp4_profile(args[0], **opts) + else: + real_main(args[0], **opts)