I finally had the time to work on my hardware assisted h264 encoding appliance. Followers of my blog (are there any) know that I got a Leopard Board DM368 (LI-TB-02) to use for my encoding project, but due to limitations of the hardware (the encoder only encodes material that is divisable by 32pixels width) I could not recode my recorded tv shows – german public tv stations air their shows in 720×576 PAL-DVD dimensions.
I also own a raspberry pi ARM computer which has a hardware assisted encoding capability. Using „raspbian“ (an arm „hardfloat“ linux distribution based on debian), mediainfo (to extract the number of streams within a recording) and a lua script, I start a GStreamer to recode the transport stream and encapsulate the resulting h264 (video) and aac (audio) materialn in a matroska container.
I’ve chosen matroska because the format seems to deal well with input (transport stream) implications. The audio stream of a mpeg transport stream is not always (or, to be more precise, almost always) not in time synchronization with the video stream (to make matters worse, the timing between the audio and video stream can shift with time). MPEG-4 containers require (as far as I understand) the streams to be in sync, which is problematic with most stream-based recoding.
Playback of the resulting mkv seems to be shaky on my laptop with vlc – playback with Raspbmc (a raspberry pi based XBMC distribution) is perfectly fine.
Helper function for luaxpath:
/mnt/media/src/recode/split.lua:
function split(str, pat) local t = {} -- NOTE: use {n = 0} in Lua-5.0 local fpat = "(.-)" .. pat local last_end = 1 local s, e, cap = str:find(fpat, 1) while s do if s ~= 1 or cap ~= "" then table.insert(t,cap) end last_end = e+1 s, e, cap = str:find(fpat, last_end) end if last_end <= #str then cap = str:sub(last_end) table.insert(t, cap) end return t end
Extracting the transport stream information:
/mnt/media/src/recode/mediainfo.lua:
package.path = '/mnt/media/src/recode/?.lua;' .. package.path require "xpath" require "lxp.lom" require "split" function mediainfo(filename) local ret = {} local path = string.match(filename, "(.-)([^\\/]-%.?([^%.\\/]*))$") os.execute("mediainfo --Output=XML " .. filename .. " > " .. filename .. ".info.xml") local xmlfile = assert(io.open(filename .. ".info.xml"), "XML Info file could not be openend") local xmlblob = xmlfile:read("*all") xmlblob = string.gsub(xmlblob, "\n", "") local xmltab = lxp.lom.parse(xmlblob) ret["fileformat"] = xpath.selectNodes(xmltab, "//Format")[1][1] ret["audio"] = xpath.selectNodes(xmltab, '/Mediainfo/File/track[@type="Audio"]') os.remove(filename .. ".info.xml") return ret, xmltab end
The script calling gstreamer:
/mnt/media/src/recode/gstrecode.lua:
#!/usr/bin/env lua package.path = '/mnt/media/src/recode/?.lua;' .. package.path require "mediainfo" local sourcefile = string.gsub(arg[1], "^\./", "") local destfile = string.gsub(arg[2], "^\./", "") print("Source: " .. sourcefile) print("Destination: " .. destfile) local lockPrevious = io.open(sourcefile .. ".lck", "r") if lockPrevious ~= nil then print("Already transcoding or stale lockfile found") os.exit() end local lock = io.open(sourcefile .. ".lck", "w") lock:write("LOCKED") lock:flush() lock:close() local info = mediainfo(sourcefile) local command = "" command = command .. "gst-launch-1.0 filesrc location=" .. sourcefile .. " ! progressreport ! " if info["fileformat"] == "MPEG-TS" then command = command .. "tsparse ! " end command = command .. " decodebin name=demux " for i = 1, table.getn(info["audio"]) do command = command .. "demux. ! queue ! audioconvert ! voaacenc bitrate=128000 ! queue ! mux. " end command = command .. 'demux. ! queue ! ' if os.getenv("HOME") == "/home/pi" then command = command .. 'omxh264enc control-rate=1 target-bitrate=1250000 ! ' else command = command .. 'x264enc bitrate=1250 ! ' end command = command .. '"video/x-h264,profile=high" ! queue ! h264parse ! mux. ' command = command .. "matroskamux name=mux ! filesink location=" .. destfile print("Command: " .. command) local retcode = os.execute(command) if retcode == 0 then os.remove(sourcefile .. ".lck") else os.remove(destfile) local failed = io.open(sourcefile .. ".failedencode", "w") failed:write("EPIC FAIL") failed:close() end
Call: /mnt/media/src/recode/gstrecode.lua source.ts destination.mkv
I use the same script for encoding on my x86 machines and the pi – that’s why I check for the home directory of the calling user. If you want to use the script, you might want to change the paths and this check. The script requires luaexpat to be installed.
A problem still occurs, if the aspect-ratio within the transport stream suddenly changes (i.e., a 16:9 show preceded by a 4:3 show) – the encoding process just stalls.