Transcoding Video To 3GP Format For Motorola VE20 Phone

From Nearline Storage
Jump to: navigation, search

Using ffmpeg to transcode video into 3GP format for my Motorola VE20 cell phone

I captured a video on the phone, copied it to Linux and probed it with ffmpeg:

 Duration: nn:nn:nn.nn, start: 0.000000, bitrate: 393 kb/s
   Stream #0.0(eng): Video: mpeg4, yuv420p, 320x240 [PAR 1:1 DAR 4:3], 30.00 tb(r)
   Stream #0.1(eng): Audio: mp4a / 0x6134706D, 8000 Hz, mono, s16

Starting from that, I constructed an ffmpeg command string that will transcode a video file to play on my Motorola VE20 phone, with better quality audio:

 ffmpeg -i "$input" -f mp4 -s 320x240 -acodec libfaac -ac 1 "$output"

This results in:

 Duration: nn:nn:nn.nn, start: 0.000000, bitrate: 208 kb/s
   Stream #0.0(und): Video: mpeg4, yuv420p, 320x240 [PAR 1:1 DAR 4:3], 29.97 tb(r)
   Stream #0.1(und): Audio: aac, 48000 Hz, mono, s16

With older versions of ffmpeg "-acodec libfaac" may not work, you may have to use "-acodec aac" instead.

ffmpeg's native 3GP support

Later versions of ffmpeg support an "-f 3gp" option if they're compiled with the "--enable-libamr-nb --enable-libamr-wb" options during the configuration step. Enabling this on my Fedora 10 system required that I rebuild the ffmpeg package with the "--with amr" rpmbuild option. This pre-reqs the installation of the libamr* packages.

The ffmpeg command line for "3gp" format files:

 ffmpeg -i "$input" -f 3gp -s qcif -ar 8000 -ac 1 -ab 12200 "$output"

The "3gp" format uses the h.263 video codec and limits your frame size choices to sqcif (128x96), qcif (176x144), cif (352x288), and 4cif (704x576). Only the smaller sizes worked on my phone but they did not fill the screen. The audio options are required by the libamr_nb codec that ffmpeg uses for the 3gb format. The small frame size and the terrible audio quality made me abandon this option in favor of the MP4/AAC option shown above.

Target location for 3GP files on Motorola VE20

Files are stored on the phone's external micro-SD card. The ffmpeg commands used here produce files that are 90-100MB per hour of video.

  • If you're using a USB cable to mount the phone as a "mass storage device" then these files go into the "dcim" directory.
  • If you're using Bluetooth to send files to the phone then the target directory is "MMC(Removeable)/DCIM".
  • If you're mounting the micro-SD card outside of the phone then the target directory is "DCIM".

Files stored in this location are played using the same procedures as if they were videos recorded by the phone's camera.

A myth2phone transcoding script much like the myth2ipod script

Like myth2ipod, this can be used to set up a user job in MythTV that will transcode recordings into 3GP format for loading on my phone. This doesn't set up podcasting like Myth2ipod does however as my phone doesn't support podcasting. I wrote a separate script that syncs the directory this writes to with the video directory on my phone via Bluetooth. See below for that.

~/bin/myth2phone:

#!/bin/sh

#  The directory where we write the transcoded video
OUTDIR="/storage/phone-files"

#  Who am I and how do I work?
ME=`basename $0`
USAGE="Usage: $ME [-c|--cut] recording-filename"

#  Parse the command line options
CUT=false
OPTS=`getopt -o c --long cut -n 'myth2phone' -- "$@"`
if [ $? != 0 ]; then
  echo "$USAGE"
  exit 1
fi
eval set -- "$OPTS"
while true; do
  case "$1" in
    -c|--cut) CUT=true; shift ;;
    --) shift; break ;;
  esac
done

#  Make sure we got a file name and that the file exists
if [ $# -ne 1 ]; then
  echo "$USAGE"
  exit 1
else
  if [ ! -e "$1" ]; then
    echo "Unable to find the file $1"
    echo "$USAGE"
    exit 1
  fi
fi

INPATH=${1%/*}
INFILE=${1##*/}
CHANNEL=${INFILE%%_*}
START=${INFILE##*_}
START="${START%%.*}"

# Show what we think we have
echo "Inputs:"
echo "  Storage path     : $INPATH"
echo "  Recording file   : $INFILE"
echo "  Channel          : $CHANNEL"
echo "  Program date/time: $START"
if $CUT; then
  echo "  Use cutlist      : True"
else
  echo "  Use cutlist      : False"
fi

#  Get database access info
source /etc/mythtv/mysql.txt

#  Get program info from the database
SQL="SELECT title, subtitle, description FROM recorded WHERE chanid = $CHANNEL AND DATE_FORMAT(starttime,'%Y%m%d%H%i%s') = $START;"
RESULTS=`nice -n18 mysql -u$DBUserName -p$DBPassword $DBName --vertical --execute="$SQL"`
if [ "$?" != "0" ]; then
  echo "Unable to get program details from the database"
  exit 1
fi
TITLE=`echo "$RESULTS" | grep -e "^ *title:" | sed 's/.*title: //; s!/!-!g'`
SUBTITLE=`echo "$RESULTS" | grep -e "^ *subtitle:" | sed 's/.*subtitle: //; s!/!-!g'`
DESCRIPTION=`echo "$RESULTS" | grep -e "^ *description:" | sed 's/.*description: //'`

echo "Program details from the database:"
echo "  Title      : $TITLE"
echo "  Subtitle   : $SUBTITLE"
echo "  Description: $DESCRIPTION"

# Figure out the name for the output file
NAME=${TITLE:0:20}
OUTNAME="$OUTDIR/$NAME ${START:0:8}.3gp"
CTR=2
while [ -e "$OUTNAME" ]; do
  OUTNAME="$OUTDIR/$NAME ${START:0:8} $CTR.3gp"
  CTR=$(( $CTR + 1 ))
done
echo "Output file name will be $OUTNAME"

if $CUT; then
  # Generate the cutlist from the flagged commercials
  echo "Generating cutlist"
  nice -n18 /usr/bin/mythcommflag --chanid "$CHANNEL" --starttime "$START" --gencutlist
  HONORCUTLIST=" --honorcutlist"
else
  HONORCUTLIST=""
fi

# Make a directory for the pipes
CTR=1
FIFO="/dev/shm/${ME}_"$RANDOM
while ! mkdir -m 755 "$FIFO"; do
  if [ $CTR -gt 10 ]; then
    echo "Unable to create directory for FIFO pipes in $CTR tries: $FIFO"
    exit 1
  fi
  CTR=$(( $CTR + 1 ))
  FIFO="/dev/shm/${ME}_"$RANDOM
done
# Use mythtranscode to send the audio and video to pipes, optionally edited according to the cutlist
echo "Starting mythtranscode"
nice -n18 mythtranscode --showprogress --profile '0' --chanid "$CHANNEL" --starttime "$START" --fifodir "$FIFO" $HONORCUTLIST 2>&1 &

# Wait for the pipes to appear
CTR=0
GOTPIPES=true
while [ ! -p "$FIFO/vidout" ]; do
  echo "Waiting for mythtranscode to make pipes available"
  sleep 1
  CTR=$(( $CTR + 1 ))
  if [ $CTR -gt 60 ]; then
    echo "MYTHTRANSCODE never made a video pipe available"
    GOTPIPES=false
  fi
done
CTR=0
while [ ! -p "$FIFO/audout" ]; do
  echo "Waiting for mythtranscode to make pipes available"
  sleep 1
  CTR=$(( $CTR + 1 ))
  if [ $CTR -gt 30 ]; then
    echo "MYTHTRANSCODE never made an audio file available"
    GOTPIPES=false
  fi
done

# Encode from the pipes with ffmpeg
if $GOTPIPES; then
  echo "Starting ffmpeg"
  nice -n18 ffmpeg -f rawvideo -s 720x480 -r 29.970 -i "$FIFO/vidout" -f yuv4mpegpipe - 2>/dev/null | \
    nice -n18 yuvdenoise 2>/dev/null | \
    nice -n18 ffmpeg -threads 2 -y -f s16le -ar 48000 -ac 2 -i "$FIFO/audout" -f yuv4mpegpipe -s 720x480 \
      -aspect 1.33333333333333 -r 29.970 -i - -aspect 1.3333 -r 29.97 -deinterlace -croptop 6 -cropright 10 \
      -cropbottom 6 -cropleft  10 -s 320x240  -f mp4 -acodec libfaac -ac 1 "$OUTNAME" &>/dev/null
fi

# Clean up
rm -fr "$FIFO"

# Delete the cutlist if we created it
if $CUT; then
  echo "Clearing cutlist"
  nice -n18 mythcommflag --chanid "$CHANNEL" --starttime "$START" --clearcutlist
fi

# Finalize the file
if [ -e "$OUTNAME" ]; then
  #  Add 3GP tags
  nice -n18 /usr/local/bin/AtomicParsley "$OUTNAME" --3gp-title "$TITLE" --3gp-description "$DESCRIPTION" \
    --3gp-year ${START:0:4} --overWrite
  chown mythtv:mythtv "$OUTNAME"
  echo "$OUTNAME is ready for your phone"
else
  echo "$ME processing failed"
fi

A script to copy the 3GP files produced by myth2phone to my phone via Bluetooth

~/bin/phoneCast:

#!/bin/sh

#  Sync files in a directory with a Bluetooth device

#  The obexftp package must be installed.  The Bluetooth device must already 
#  have been set up to trust the system that's running this script

SOURCE="/mnt/mythtv/phone-files"
DEVICE_ID="00:1F:7E:8E:FC:93"
TARGET="MMC(Removable)/dcim"
WIRED_MOUNTPOINT="/media/NO LABEL"
#  Size limit for device
MAX_SIZE=3500000

#  -b specify a different DEVICE_ID
#  -h prints help
#  -q suppress progress messages
#  -s specify a different SOURCE
#  -t specify a different TARGET
#  -w use wired connection (/media/NO LABEL mount point)
MSGS="yes"
WIRED="no"
while getopts b:hqs:t:w OPT ; do
  case "$OPT" in 
    q) MSGS="no";;
    b) DEVICE_ID="$OPTARG";;
    s) SOURCE="$OPTARG";;
    t) TARGET="$OPTARG";;
    w) TARGET="$WIRED_MOUNTPOINT/dcim"
       WIRED="yes";;
    *) ME=`basename $0`
       echo "$ME [-b device_id] [-h] [-q] [-s source_path] [-t target_path] [-w]"
       echo "   -b : use device_id to specify the Bluetooth device instead of the"
       echo "        device id hard coded into the script, $DEVICE_ID"
       echo "   -h : print this help"
       echo "   -q : suppress progress messages"
       echo "   -s : use source_path as the source for files that are to be synced"
       echo "        with those on the device, instead of the path hard coded into"
       echo "        the script, $SOURCE"
       echo "   -t : use target_path as the destination directory on the device"
       echo "        instead of the path hard coded into the script, $TARGET"
       echo "   -w : use a wired connection to the device, mounted at $WIRED_MOUNTPOINT"
       exit 1;;
  esac
done

#  Function called to print progress messages
function echo_msg () {
  if [ "$MSGS" = "yes" ] ; then 
    echo "$*"
  fi
}

#  Check obexftp is installed
if ! which obexftp &>/dev/null ; then
  echo_msg "The obexftp package must be installed to use this script"
  exit 1
fi

#  Is the target even accessible?
if [ "$WIRED" = "yes" ]; then
  if [ ! -d "$TARGET" ]; then
    echo_msg "Device not accessible"
    exit 1
  fi
else
  if obexftp --bluetooth $DEVICE_ID --list "$TARGET" 2>&1 | grep -q "^failed:"; then
    echo_msg "Device not accessible"
    exit 1
  fi
fi

#  Keep the total size of the source directory under $MAX_SIZE bytes so that we don't overfill the device
if [ `du -s "$SOURCE" | cut -f 1` -gt $MAX_SIZE ] ; then 
  echo_msg "Using more than $MAX_SIZE bytes, deleting oldest files from $SOURCE and device"
  oIFS=$IFS
  IFS=$'\n'
  #  List files in create-time order, sorted oldest to newest
  for FILE in `ls -ltcr "$SOURCE" | sed 's/[^:]*....//'` ; do
    IFS=$oIFS
    if [ "$FILE" != "" ]; then
      #  Delete the file on the device
      if [ "$WIRED" = "yes" ]; then
        rm "$TARGET/$FILE" &>/dev/null
        RC=$?
      else
        obexftp --bluetooth $DEVICE_ID --chdir "$TARGET" --delete "$FILE" &>/dev/null
        RC=$?
      fi
      if [ ! $RC ]; then
        echo_msg  "    Deleting $FILE from device failed, terminating"
        exit 1
      else
        echo_msg  "    Deleted $FILE from the device"
        #  Delete the source file
        if ! sudo rm -f "$SOURCE/$FILE" ; then
          echo_msg "    Unable to delete $SOURCE/$FILE, terminating"
          exit 1
        else
          echo_msg "    Deleted $SOURCE/$FILE"
        fi
      fi
      #  If we're small enough now, break out of this loop
      if [ `du -s "$SOURCE" | cut -f 1` -lt $MAX_SIZE ] ; then
        echo_msg "    Ok, that's enough deleting"
        break
      fi
      sleep 15
    fi
    IFS=$'\n'
  done
  IFS=$oIFS
fi

#  Get a list of files from the device
if [ "$WIRED" = "yes" ]; then
  FL=`ls "$TARGET" 2>/dev/null`
  RC=$?
else
  FL=`obexftp --bluetooth $DEVICE_ID --list "$TARGET" 2>/dev/null`
  RC=$?
fi
if [ $RC ]; then
  oIFS=$IFS
  IFS=$'\n'
  #  For every file in the source directory
  for FILE in `ls -cr "$SOURCE"` ; do
    IFS=$oIFS
    #  If it isn't already on the phone
    echo_msg  "Considering $FILE for transmission to device"
    if [ "$WIRED" = "yes" ]; then
      LOOKFOR="$FILE"
    else
      LOOKFOR="name=\"$FILE\""
    fi
    if ! echo "$FL" | grep -q "$LOOKFOR" ; then 
      #  Put it there
      echo_msg "    It's not on the device, sending it"
      NOW=`date +"%s"`
      if [ "$WIRED" = "yes" ]; then
        cp "$SOURCE/$FILE" "$TARGET" &>/dev/null
        RC=$?
      else
        obexftp --bluetooth $DEVICE_ID --chdir "$TARGET" --put "$SOURCE/$FILE" &>/dev/null
        RC=$?
      fi
      if [ ! $RC ]; then
        echo_msg  "    Transmission failed"
      else
        SPEED=`date +"%s"`
        SIZE=`stat -c%s "$SOURCE/$FILE"`
        if [ $SPEED -eq $NOW ]; then    # Prevent division by zero
          SPEED=$(( $NOW + 1 ))
        fi
        SPEED=`echo "scale=2; $SIZE / ($SPEED - $NOW)" | bc`
        echo_msg  "    Sent ($SPEED B/s)"
      fi
      sleep 15
    else
      echo_msg  "    It's already on the device"
    fi
    IFS=$'\n'
  done
  IFS=$oIFS

  #  Delete any files that are on the device but not in the source directory
  #  Also compare file sizes, this catches files uploaded while they were 
  #  still being transcoded
  if [ -d "$SOURCE" ]; then   # But only if the source directory is accessible
    #  For every file on the device
    if [ "$WIRED" = "yes" ]; then
      FL=`ls "$TARGET" 2>/dev/null`
    else
      FL=`obexftp --bluetooth $DEVICE_ID --list "$TARGET" 2>/dev/null | grep "<file name="`
    fi
    oIFS=$IFS
    IFS=$'\n'
    for FILE in $FL; do
      IFS=$oIFS
      if [ "$WIRED" = "yes" ]; then
        SIZE=`stat -c%s "$TARGET/$FILE"`
      else
        SIZE=`echo "$FILE" | sed 's/.* size=\"\([^\"]*\)\".*/\1/'`
        FILE=`echo "$FILE" | sed 's/.*<file name=\"\([^\"]*\)\".*/\1/'`
      fi
      echo_msg  "Considering $FILE for deletion"
      #  If the associated source file is gone or if the sizes don't match
      if [ ! -e "$SOURCE/$FILE" ] || [ $SIZE -ne `stat -c%s "$SOURCE/$FILE"` ] ; then
        #  Delete the file from the device
        if [ ! -e "$SOURCE/$FILE" ] ; then
          echo_msg "    It's gone from $SOURCE, deleting it from device"
        else
          echo_msg "    Sizes don't match, deleting it from device" 
        fi
        if [ "$WIRED" = "yes" ]; then
          rm "$TARGET/$FILE" &>/dev/null
          RC=$?
        else
          obexftp --bluetooth $DEVICE_ID --chdir "$TARGET" --delete "$FILE" &>/dev/null
          RC=$?
        fi
        if [ ! $RC ]; then
          echo_msg  "    Deletion failed"
          exit 1
        else
          echo_msg  "    Deleted"
        fi
        sleep 15
      else
        echo_msg  "    Still present in $SOURCE, file sizes match, won't delete"
      fi
      IFS=$'\n'
    done 
    IFS=$oIFS
  fi

  #  Flush the buffers if we're wired in
  if [ "$WIRED" = "yes" ]; then
    sync
  fi

else
  echo_msg "Can't communicate with the device"
fi