MediaTomb and PS3: The Lazy Man's solution (Part II)

A while back I did an article on a catch-all solution for streaming content to the Playstation 3 using MediaTomb and a big transcoding script. It served me well: every movie I had - be it avi, mkv, mp4, dvd iso, RealMedia, ... was properly handled. I didn't care for HD content, mainly because my TV was a plain old CRT tube and couldn't tell the difference between a 720x480 mpeg2 and a 1080p h.264 movie anyway.

Last month I did get me a HDTV finally, and my original transcoding script wasn't ready for it.

Last week I finally got me a Boxee Box. Since this thing does everything I want and more when it comes to movies and series, I don't have any use for mediatomb anymore. So, while I'll keep the articles online... I mostlikely won't be making any changes/bugfixes to the script anymore :-)

In hindsight, there were two crucial things wrong with my original setup:

  • I didn't check which codecs were used in the source file. Everything was transcoded to MPEG-2, even if the source codec was supported by the PS3.
  • Transcoding HD content was problematic: transcoding 720p worked (barely!), but 1080p was plain impossible, cpu-wise.

So I had two major points to fix: first of all checking the source file's internals, and related to that, remuxing where possible instead of transcoding. It took me a few days of trial-and-error, but this is the result for now:

#!/bin/bash
#
# General all-covering MediaTomb transcoding script.
#
#############################################################################
# Edit the parameters below to suit your needs.
#############################################################################

# Subtitles imply transcoding; set to 1 to disable subtitle rendering.
# For divx this doesn't matter much but for mp4, mkv and DVD it does.
DISABLESUBS=1

# Change this to enable different DVD subtitle languages.
SUBS="nl,en"

# Change this line to set the average bitrate.
# Use something like 8000 for wired connections; lower to 2000 for wireless.
AVBIT=8000

# Change this line to set the maximum bitrate.
# Use something like 12000 for wired connections; lower to 5000 for wireless.
MVBIT=12000

# Change this line to set the MPEG audio bitrate in kbps. AC3 is fixed to 384.
AABIT=256

# Change this line to set your favourite subtitle font.
SFONT="/etc/mediatomb/DejaVuSans.ttf"

# Change this line to set the size of subtitles. 20-25 is okay.
SUBSIZE=20

# Enable downscaling of transcoded content to 720 pixels wide (DVD format)?
DOWNSCALE=1

# If downscaling is enabled, anything over this width (pixels) will be downscaled.
MAXSIZE=900

# Enable logging to file?
LOGGING=1

# If logging is enabled, log to which file?
LOGFILE="/var/log/mediatomb-transcode.log"

#############################################################################
# Do not change anything below this line.
#############################################################################
# Variables
#############################################################################

FILE=$1
VERSION="0.12"

MENCODER=$(which mencoder)
MEDIAINFO=$(which mediainfo)
FFMPEG=$(which ffmpeg)
LSDVD=$(which lsdvd)
XML=$(which xmlstarlet)

M_TR_M="-oac lavc -ovc lavc -of mpeg -lavcopts \
    abitrate=${AABIT}:vcodec=mpeg2video:keyint=1:vbitrate=${AVBIT}:\
    vrc_maxrate=${MVBIT}:vrc_buf_size=1835 \
    -mpegopts muxrate=12000 -af lavcresample=44100 "
M_TR_A="-oac lavc -ovc copy -of mpeg -lavcopts \
    abitrate=${AABIT} -af lavcresample=44100 "
M_RE_M="-oac copy -ovc copy -of mpeg -mpegopts format=dvd -noskip -mc 0 "
F_TR_M="-acodec ac3 -ab 384k -vcodec copy -vbsf h264_mp4toannexb -f mpegts -y "
F_RE_M="-acodec copy -vcodec copy -vbsf h264_mp4toannexb -f mpegts -y "
SUBOPTS="-slang ${SUBS} "
SRTOPTS="-font ${SFONT} -subfont-autoscale 0 \
    -subfont-text-scale ${SUBSIZE} -subpos 100 "
SIZEOPTS="-vf harddup,scale=720:-2 "
NOSIZEOPTS="-vf harddup "
S24FPS="23.976"
S24OPT="-ofps 24000/1001 "
S30FPS="29.970"
S30OPT="-ofps 30000/1001 "

VCODEC=""
ACODEC=""
VWIDTH=""
VFPS=""
QPEL=""
AVCPROF=""

OPTS=("")

declare -i MODE=0

#############################################################################
# Functions
#############################################################################

function log {
        if [ "${LOGGING}" == "1" ] ; then
                echo -e "$(date +'%Y/%m/%d %H:%m:%S') \t $1" >> ${LOGFILE}
        fi
}

function mediainfo {
        MIOUT=$(mktemp /tmp/tmp.mediainfo.XXXXXX)
        log "Logging mediainfo XML to ${MIOUT}."
        ${MEDIAINFO} --output=xml "${FILE}" > ${MIOUT}
        VCODEC=$(${XML} sel -t -m ".//track[@type='Video']" -v "Format" ${MIOUT} )
        ACODEC=$(${XML} sel -t -m ".//track[@type='Audio']" -v "Format" ${MIOUT} )
        VWIDTH=$(${XML} sel -t -m ".//track[@type='Video']" -v "Width" \ 
            ${MIOUT} | sed 's/ pixels//' )
        VFPS=$(${XML} sel -t -m ".//track[@type='Video']" -v "Frame_rate" \ 
            ${MIOUT} | sed 's/ fps//' )
        AVCPROF=$(${XML} sel -t -m ".//track[@type='Video']" -v "Format_profile" \
            ${MIOUT} | sed 's/[^0-9]//g' )
        QPEL=$(${XML} sel -t -m ".//track[@type='Video']" -v "Format_settings__QPel" \
            ${MIOUT} )
        log "Variables found: \
            ${VCODEC} | ${ACODEC} | ${VWIDTH} | ${VFPS} | ${AVCPROF} | ${QPEL} "
        rm -f ${MIOUT}
}

function tropts {
        if [ "${DOWNSCALE}" == "1" -a ${VWIDTH} -gt ${MAXSIZE} ] ; then
                log "Rescaling to 720 pixels wide."
                OPTS+=(${SIZEOPTS})
        else
                log "Rescaling disabled or file within limits."
                OPTS+=(${NOSIZEOPTS})
        fi
        if [ "${VFPS}" == "${S24FPS}" ] ; then
                log "Framerate adjusted for mencoder."
                OPTS+=(${S24OPT})
        else if [ "${VFPS}" == "${S30FPS}" ] ; then
                log "Framerate adjusted for mencoder."
                OPTS+=(${S30OPT})
        else
                log "Framerate acceptable for mencoder."
        fi
        fi
}

function getmode {
        # Fixed case: DVD ISO.
        if [ "${FEXT}" == "ISO" ] ; then
                CHAPTER=$(${LSDVD} "${FILE}" | grep Longest | sed 's/.* //')
                log "DVD iso image found: Longest chapter is ${CHAPTER}."
                MODE+=${DISABLESUBS}1000000
                return 0
        fi
        # Fixed case: subtitle found: transcode by default.
        if [ "${DISABLESUBS}" == "0" -a -e "$(echo $FILE | sed 's/...$/sub/')" ] ; then
                log "SRT subtitle found."
                SUB=$(echo $FILE | sed 's/...$/sub/')
                MODE+=100000
                return 0
        elif [ "${DISABLESUBS}" == "0" -a -e "$(echo $FILE | sed 's/...$/srt/')" ] ; then
                log "SUB subtitle found."
                SUB=$(echo $FILE | sed 's/...$/srt/')
                MODE+=100000
                return 0
        fi

        log "No subtitles found, or subtitle rendering disabled."
        mediainfo

        case ${VCODEC} in
        "AVC")
                if [ "${AVCPROF}" -gt "41" ] ; then
                        # Cannot handle h.264 4.1+
                        MODE+=10000     
                else          
                        # We can handle the rest                          
                        MODE+=1         
                fi ;;
        "MPEG-4 Visual")
                if [ "${QPEL}" == "No" ] ; then
                        # No QPEL: we could remux the video         
                        MODE+=100       
                else            
                        # QPEL: just transcode it all                        
                        MODE+=10000     
                fi ;;
        * )
                        # Transcode everything we don't know
                        MODE+=10000 ;;  
        esac

        case ${ACODEC} in
        "AC-3" | "MPEG Audio" )      
                        # These should be wellknown                   
                        MODE+=1 ;;      
        * )
                if [ "${MODE}" -lt "100" ] ; then    
                        # If video is AVC, transcode audio in m2ts   
                        MODE+=10        
                else     
                        # Otherwise in other container                       
                        MODE+=1000      
                fi ;;
        esac

}

function processmode {
        log "Mode is ${MODE}."
        if [ ! "${MODE}" -lt "10000000" ] ; then
                EXEC="${MENCODER} -dvd-device"
                OPTS+=(dvd://${CHAPTER} ${M_RE_M} -o )
        elif [ ! "${MODE}" -lt "1000000" ] ; then
                EXEC="${MENCODER} -dvd-device"
                OPTS+=(dvd://${CHAPTER} ${SUBOPTS} ${M_TR_M} -o )
        elif [ ! "${MODE}" -lt "100000" ] ; then
                EXEC=${MENCODER}
                tropts
                OPTS+=(${M_TR_M} -sub ${SUB} ${SRTOPTS} -o )
        elif [ ! "${MODE}" -lt "10000" ] ; then
                EXEC=${MENCODER}
                tropts
                OPTS+=(${M_TR_M} -o )
        elif [ ! "${MODE}" -lt "1000" ] ; then
                EXEC=${MENCODER}
                tropts
                OPTS+=(${M_TR_M} -o)
        elif [ ! "${MODE}" -lt "100" ] ; then
                EXEC=${MENCODER}
                OPTS+=(${M_TR_M} -o)
        elif [ ! "${MODE}" -lt "10" ] ; then
                EXEC="${FFMPEG} -i"
                OPTS+=(${F_TR_M})
        elif [ ! "${MODE}" -lt "1" ] ; then
                EXEC="${FFMPEG} -i"
                OPTS+=(${F_RE_M})
        else
                log "I'm sorry Dave, I'm afraid I can't do mode=0."
        fi
}

#############################################################################
# Main method
#############################################################################

log "Starting MediaTomb Multifunctional Transcoder (version ${VERSION})."
FEXT=$(echo $FILE | sed 's/.*\.//' | tr [a-z] [A-Z])
log "${FEXT} file specified: \"${FILE}\""

getmode
processmode

log "Starting exec:"
log "${EXEC} \"${FILE}\" ${OPTS[@]} ${2} &>/dev/null"
exec ${EXEC} "${FILE}" ${OPTS[@]} "${2}" &>/dev/null

Some explanation on how this works: first of all, the script checks the source file extension. Unlike the old script however, this is no longer the final source of info. A variable "MODE" has been introduced, which is being modified by various checks based on mediainfo output of the source file. In short: every digit has a meaning. If MODE has one digit, it means that both audio and video have been checked, and can be remuxed to a container the PS3 supports. Most common example: h.264 video, AC3 audio. If MODE has two digits after running through the checks, the video stream can be remuxed, but the audio stream needs to be transcoded. This happens quite a lot with MP4s where the video codec works well, but the PS3 has issues with the audio stream.

Three or four digits is a case I haven't cracked yet. Basically, three digits means that both audio and video are MPEG-4 (DivX, XviD) codecs that are natively supported by the PS3, so in theory, we might remux those into a container and serve it. You'll notice that most AVIs will have such a properly supported video stream and MP3 audio; they end up in this case. Four digits means only the audio needs to be transcoded, and the video can stay. Big problem there: the PS3 doesn't like it when you serve it an AVI that you've remuxed... something for me to investigate. For now, they get the same treatment as the five-digit MODEs - transcoded into a big ol' MPEG-2 stream, like all the other unsupported crap out there.

Six digits means any random media with a subtitle, which results in MPEG-2. Seven means a transcoded DVD iso (with subtitles); eight is a remuxed DVD iso (without subtitles). And nothing stops you from adding more subclasses if you'd see a need for it.

Once the MODE is set, all the script needs to do is pick the proper encoder (ffmpeg or mencoder), add the proper options, and exec it.

You'll also notice an extra dependency was introduced: XMLStarlet. This is needed to propery parse the XML dump that is created by running mediatomb on a source file. Well, 'needed'... it makes life a hell of a lot easier. And this way you don't have to call mediainfo half a dozen times on the same file to pull it through grep.

All in all, I'm happy with the result. I still need to investigate on how to remux an avi using this script (which has mimetype video/mpeg in config.xml; might have something to do with it?) but that's something for later.

Enjoy!