抓取一个包含H.264 Payload RTP包的SIP会话或RTSP会话后,用Wireshark的Play功能只能播放声音,不能播放视频。把RTP payload直接导出成文件后也是不能直接播放的,因为H.264 over RTP封包是符合RFC3984规范的,必须按照该规范把H.264数据取出来后,组成NALU,放到avi/mp4或裸码流文件等容器里后才能播放。
本人写了一个wireshark插件,可以在打开包含H.264码流的抓包后,选菜单“Tools->Export H264 to file [HQX's plugins]”后,把抓包文件里的H.264码流自动导出到抓包文件所在目录(工作目录)里,名为from_<RTP流源ip>_<RTP流源端口>_to_<RTP流目的ip>_<RTP流目的端口>.264的264裸码流文件里。(文件格式为每个NALU前加0x00000001分隔符)。
本程序可以识别RFC3984里提到的三种H.264 over RTP封装,分别是Single NALU(一个RTP含一个NALU)、STAP-A(一个RTP包含多个NALU)、FU-A(一个NALU分布到多个RTP包)三种封装格式,且会自动把SPS和PPS放到裸码流文件头部。
Lua脚本如下:
-- Dump RTP h.264 payload to raw h.264 file (*.264) -- According to RFC3984 to dissector H264 payload of RTP to NALU, and write it -- to from<sourceIp_sourcePort>to<dstIp_dstPort>.264 file. By now, we support single NALU, -- STAP-A and FU-A format RTP payload for H.264. -- You can access this feature by menu "Tools->Export H264 to file [HQX's plugins]" -- Author: Huang Qiangxiong ([email protected]) -- change log: -- 2012-03-13 -- Just can play ------------------------------------------------------------------------------------------------ do -- for geting h264 data (the field's value is type of ByteArray) local f_h264 = Field.new("h264") -- menu action. When you click "Tools->Export H264 to file [HQX's plugins]" will run this function local function export_h264_to_file() -- window for showing information local tw = TextWindow.new("Export H264 to File Info Win") local pgtw = ProgDlg.new("Export H264 to File Process", "Dumping H264 data to file...") -- add message to information window function twappend(str) tw:append(str) tw:append("\n") end -- running first time for counting and finding sps+pps, second time for real saving local first_run = true -- variable for storing rtp stream and dumping parameters local stream_infos = {} -- trigered by all h264 packats local my_h264_tap = Listener.new(tap, "h264") -- get rtp stream info by src and dst address function get_stream_info(pinfo) local key = "from_" .. tostring(pinfo.src) .. "_" .. tostring(pinfo.src_port) .. "to" .. tostring(pinfo.dst) .. "_" .. tostring(pinfo.dst_port) local stream_info = stream_infos[key] if not stream_info then -- if not exists, create one stream_info = { } stream_info.filename = key.. ".264" stream_info.file = io.open(stream_info.filename, "wb") stream_info.counter = 0 -- counting h264 total NALUs stream_info.counter2 = 0 -- for second time running stream_infos[key] = stream_info twappend("Ready to export H.264 data (RTP from " .. tostring(pinfo.src) .. ":" .. tostring(pinfo.src_port) .. " to " .. tostring(pinfo.dst) .. ":" .. tostring(pinfo.dst_port) .. " to file:\n [" .. stream_info.filename .. "] ...\n") end return stream_info end -- write a NALU or part of NALU to file. function write_to_file(stream_info, str_bytes, begin_with_nalu_hdr) if first_run then stream_info.counter = stream_info.counter + 1 if begin_with_nalu_hdr then -- save SPS or PPS local nalu_type = bit.band(str_bytes:byte(0,1), 0x1F) if not stream_info.sps and nalu_type == 7 then stream_info.sps = str_bytes elseif not stream_info.pps and nalu_type == 8 then stream_info.pps = str_bytes end end else -- second time running if stream_info.counter2 == 0 then -- write SPS and PPS to file header first if stream_info.sps then stream_info.file:write("\00\00\00\01") stream_info.file:write(stream_info.sps) else twappend("Not found SPS for [" .. stream_info.filename .. "], it might not be played!\n") end if stream_info.pps then stream_info.file:write("\00\00\00\01") stream_info.file:write(stream_info.pps) else twappend("Not found PPS for [" .. stream_info.filename .. "], it might not be played!\n") end end if begin_with_nalu_hdr then -- *.264 raw file format seams that every nalu start with 0x00000001 stream_info.file:write("\00\00\00\01") end stream_info.file:write(str_bytes) stream_info.counter2 = stream_info.counter2 + 1 if stream_info.counter2 == stream_info.counter then stream_info.file:flush() twappend("File [" .. stream_info.filename .. "] generated OK!\n") end -- update progress window's progress bar if stream_info.counter > 0 then pgtw:update(stream_info.counter2 / stream_info.counter) end end end -- read RFC3984 about single nalu/stap-a/fu-a H264 payload format of rtp -- single NALU: one rtp payload contains only NALU function process_single_nalu(stream_info, h264) write_to_file(stream_info, h264:tvb()():string(), true) end -- STAP-A: one rtp payload contains more than one NALUs function process_stap_a(stream_info, h264) local h264tvb = h264:tvb() local offset = 1 repeat local size = h264tvb(offset,2):uint() write_to_file(stream_info, h264tvb(offset+2, size):string(), true) offset = offset + 2 + size until offset >= h264tvb:len() end -- FU-A: one rtp payload contains only one part of a NALU (might be begin, middle and end part of a NALU) function process_fu_a(stream_info, h264) local h264tvb = h264:tvb() local fu_idr = h264:get_index(0) local fu_hdr = h264:get_index(1) if bit.band(fu_hdr, 0x80) ~= 0 then -- start bit is set then save nalu header and body local nalu_hdr = bit.bor(bit.band(fu_idr, 0xE0), bit.band(fu_hdr, 0x1F)) write_to_file(stream_info, string.char(nalu_hdr), true) else -- start bit not set, just write part of nalu body end write_to_file(stream_info, h264tvb(2):string(), false) end -- call this function if a packet contains h264 payload function my_h264_tap.packet(pinfo,tvb) local h264s = { f_h264() } -- using table because one packet may contains more than one RTP for i,h264_f in ipairs(h264s) do if h264_f.len < 2 then return end local h264 = h264_f.value -- is ByteArray local hdr_type = bit.band(h264:get_index(0), 0x1F) local stream_info = get_stream_info(pinfo) if hdr_type > 0 and hdr_type < 24 then -- Single NALU process_single_nalu(stream_info, h264) elseif hdr_type == 24 then -- STAP-A Single-time aggregation process_stap_a(stream_info, h264) elseif hdr_type == 28 then -- FU-A process_fu_a(stream_info, h264) else twappend("Error: unknown type=" .. hdr_type .. " ; we only know 1-23(Single NALU),24(STAP-A),28(FU-A)!") end end end -- close all open files function close_all_files() if stream_infos then for id,stream in pairs(stream_infos) do if stream and stream.file then stream.file:close() stream.file = nil end end end end function my_h264_tap.reset() -- do nothing now end function remove() close_all_files() my_h264_tap:remove() end tw:set_atclose(remove) -- first time it runs for counting h.264 packets and finding SPS and PPS retap_packets() first_run = false -- second time it runs for saving h264 data to target file. retap_packets() -- close progress window pgtw:close() end -- Find this feature in menu "Tools->"Export H264 to file [HQX's plugins]"" register_menu("Export H264 to file [HQX's plugins]", export_h264_to_file, MENU_TOOLS_UNSORTED) end
把代码保存成h264_export.lua文件,放到wireshark安装目录下,然后修改wireshark安装目录下的init.lua文件:
(1)若有disable_lua = true这样的行,则注释掉;
(2)在文件末加入dofile("h264_export.lua")
重新打开wirekshark就能使用该功能了。
另外,264裸码流文件一般播放器不一定能播放,推荐使用ffmpeg的ffplay播放,或用ffmpeg转成通用文件格式播放。
2014年升级版,支持排序、丢弃不完整帧,注意生成的文件from...在抓拍文件相同的目录:
-- Dump RTP h.264 payload to raw h.264 file (*.264) -- According to RFC3984 to dissector H264 payload of RTP to NALU, and write it -- to from<sourceIp_sourcePort>to<dstIp_dstPort>.264 file. By now, we support single NALU, -- STAP-A and FU-A format RTP payload for H.264. -- You can access this feature by menu "Tools->Export H264 to file [HQX's plugins]" -- Author: Huang Qiangxiong ([email protected]) -- change log: -- 2012-03-13 -- Just can play -- 2012-04-28 -- Add local to local function, and add [local bit = require("bit")] to prevent -- bit recleared in previous file. -- 2013-07-11 -- Add sort RTP and drop uncompleted frame option. -- 2013-07-19 -- Do nothing when tap is triggered other than button event. -- Add check for first or last packs lost of one frame. ------------------------------------------------------------------------------------------------ do local bit = require("bit") -- for geting h264 data (the field's value is type of ByteArray) local f_h264 = Field.new("h264") local f_rtp = Field.new("rtp") local f_rtp_seq = Field.new("rtp.seq") local f_rtp_timestamp = Field.new("rtp.timestamp") local nalu_type_list = { [0] = "Unspecified", [1] = "P/B_slice", [2] = "P/B_A", [3] = "P/B_B", [4] = "P/B_C", [5] = "I_slice", [6] = "SEI", [7] = "SPS", [8] = "PPS", [9] = "AUD", } local function get_enum_name(list, index) local value = list[index] return value and value or "Unknown" end -- menu action. When you click "Tools->Export H264 to file [HQX's plugins]" will run this function local function export_h264_to_file() -- window for showing information local tw = TextWindow.new("Export H264 to File Info Win") --local pgtw = ProgDlg.new("Export H264 to File Process", "Dumping H264 data to file...") local pgtw; -- add message to information window function twappend(str) tw:append(str) tw:append("\n") end -- running first time for counting and finding sps+pps, second time for real saving local first_run = true -- variable for storing rtp stream and dumping parameters local stream_infos = nil -- drop_uncompleted_frame local drop_uncompleted_frame = false -- max frame buffer size local MAX_FRAME_NUM = 3 -- trigered by all h264 packats local my_h264_tap = Listener.new(tap, "h264") -- get rtp stream info by src and dst address function get_stream_info(pinfo) local key = "from_" .. tostring(pinfo.src) .. "_" .. tostring(pinfo.src_port) .. "to" .. tostring(pinfo.dst) .. "_" .. tostring(pinfo.dst_port) .. (drop_uncompleted_frame and "_dropped" or "_all") local stream_info = stream_infos[key] if not stream_info then -- if not exists, create one stream_info = { } stream_info.filename = key.. ".264" stream_info.file = io.open(stream_info.filename, "wb") stream_info.counter = 0 -- counting h264 total NALUs stream_info.counter2 = 0 -- for second time running stream_infos[key] = stream_info twappend("Ready to export H.264 data (RTP from " .. tostring(pinfo.src) .. ":" .. tostring(pinfo.src_port) .. " to " .. tostring(pinfo.dst) .. ":" .. tostring(pinfo.dst_port) .. " to file:\n [" .. stream_info.filename .. "] ...\n") end return stream_info end -- write a NALU or part of NALU to file. local function real_write_to_file(stream_info, str_bytes, begin_with_nalu_hdr) if first_run then stream_info.counter = stream_info.counter + 1 if begin_with_nalu_hdr then -- save SPS or PPS local nalu_type = bit.band(str_bytes:byte(0,1), 0x1F) if not stream_info.sps and nalu_type == 7 then stream_info.sps = str_bytes elseif not stream_info.pps and nalu_type == 8 then stream_info.pps = str_bytes end end else -- second time running --[[ if begin_with_nalu_hdr then -- drop AUD local nalu_type = bit.band(str_bytes:byte(0,1), 0x1F) if nalu_type == 9 then return; end end ]] if stream_info.counter2 == 0 then -- write SPS and PPS to file header first if stream_info.sps then stream_info.file:write("\00\00\00\01") stream_info.file:write(stream_info.sps) else twappend("Not found SPS for [" .. stream_info.filename .. "], it might not be played!\n") end if stream_info.pps then stream_info.file:write("\00\00\00\01") stream_info.file:write(stream_info.pps) else twappend("Not found PPS for [" .. stream_info.filename .. "], it might not be played!\n") end end if begin_with_nalu_hdr then -- *.264 raw file format seams that every nalu start with 0x00000001 stream_info.file:write("\00\00\00\01") end stream_info.file:write(str_bytes) stream_info.counter2 = stream_info.counter2 + 1 -- update progress window's progress bar if stream_info.counter > 0 and stream_info.counter2 < stream_info.counter then pgtw:update(stream_info.counter2 / stream_info.counter) end end end local function comp_pack(p1, p2) if math.abs(p2.seq - p1.seq) < 1000 then return p1.seq < p2.seq else -- seqeunce is over 2^16, so the small one is much big return p1.seq > p2.seq end end local function print_seq_error(stream_info, str) if stream_info.seq_error_counter == nil then stream_info.seq_error_counter = 0 end stream_info.seq_error_counter = stream_info.seq_error_counter + 1 twappend(str .. " SeqErrCounts=" .. stream_info.seq_error_counter) end local function sort_and_write(stream_info, frame) table.sort(frame.packs, comp_pack) -- check if it is uncompleted frame local completed = true for i = 1, #frame.packs - 1, 1 do local seq1 = frame.packs[i].seq local seq2 = frame.packs[i+1].seq if bit.band(seq1+1, 0xFFFF) ~= seq2 then print_seq_error(stream_info, " RTP pack Lost: timestamp=" .. frame.timestamp .. " seq between " .. seq1 .. " and " .. seq2) completed = false end end if not frame.packs[1].nalu_begin then print_seq_error(stream_info, " RTP pack Lost: timestamp=" .. frame.timestamp .. " seq before " .. frame.packs[1].seq) completed = false end if not frame.packs[#frame.packs].nalu_end then print_seq_error(stream_info, " RTP pack Lost: timestamp=" .. frame.timestamp .. " seq after " .. frame.packs[#frame.packs].seq) completed = false end if completed then for i = 1, #frame.packs, 1 do real_write_to_file(stream_info, frame.packs[i].data, frame.packs[i].nalu_begin) end else twappend(" We drop one uncompleted frame: rtp.timestamp=" .. frame.timestamp .. " nalu_type=" .. frame.nalu_type .."(" .. get_enum_name(nalu_type_list, frame.nalu_type) .. ")") end end local function write_to_file(stream_info, str_bytes, begin_with_nalu_hdr, timestamp, seq, end_of_nalu) if drop_uncompleted_frame and not first_run then -- sort and drop uncompleted frame if stream_info.frame_buffer_size == nil then stream_info.frame_buffer_size = 0 end if timestamp < 0 or seq < 0 then twappend(" Invalid rtp timestamp (".. timestamp .. ") or seq (".. seq .. ")! We have to write it to file directly!") real_write_to_file(stream_info, str_bytes, begin_with_nalu_hdr) return; end -- check if this frame has existed local p = stream_info.frame_buffer while p do if p.timestamp == timestamp then break; else p = p.next end end if p then -- add this pack to frame if begin_with_nalu_hdr then p.nalu_type = bit.band(str_bytes:byte(1), 0x1F) end table.insert(p.packs, { ["seq"] = seq, ["data"] = str_bytes , ["nalu_begin"] = begin_with_nalu_hdr, ["nalu_end"] = end_of_nalu }) return end if stream_info.frame_buffer_size >= MAX_FRAME_NUM then -- write the most early frame to file sort_and_write(stream_info, stream_info.frame_buffer) stream_info.frame_buffer = stream_info.frame_buffer.next stream_info.frame_buffer_size = stream_info.frame_buffer_size - 1 end -- create a new frame buffer for new frame (timestamp) local frame = {} frame.timestamp = timestamp if begin_with_nalu_hdr then frame.nalu_type = bit.band(str_bytes:byte(1), 0x1F) end frame.packs = {{ ["seq"] = seq, ["data"] = str_bytes, ["nalu_begin"] = begin_with_nalu_hdr, ["nalu_end"] = end_of_nalu}} -- put pack to index 1 pos frame.next = nil if stream_info.frame_buffer_size == 0 then -- first frame stream_info.frame_buffer = frame else p = stream_info.frame_buffer while p.next do p = p.next end p.next = frame end stream_info.frame_buffer_size = stream_info.frame_buffer_size + 1 else -- write data direct to file without sort or frame drop real_write_to_file(stream_info, str_bytes, begin_with_nalu_hdr) end end -- read RFC3984 about single nalu/stap-a/fu-a H264 payload format of rtp -- single NALU: one rtp payload contains only NALU local function process_single_nalu(stream_info, h264, timestamp, seq) write_to_file(stream_info, h264:tvb()():string(), true, timestamp, seq, true) end -- STAP-A: one rtp payload contains more than one NALUs local function process_stap_a(stream_info, h264, timestamp, seq) local h264tvb = h264:tvb() local offset = 1 local i = 1 repeat local size = h264tvb(offset,2):uint() write_to_file(stream_info, h264tvb(offset+2, size):string(), true, timestamp, i, true) offset = offset + 2 + size i = i + 1 until offset >= h264tvb:len() end -- FU-A: one rtp payload contains only one part of a NALU (might be begin, middle and end part of a NALU) local function process_fu_a(stream_info, h264, timestamp, seq) local h264tvb = h264:tvb() local fu_idr = h264:get_index(0) local fu_hdr = h264:get_index(1) local end_of_nalu = (bit.band(fu_hdr, 0x40) ~= 0) if bit.band(fu_hdr, 0x80) ~= 0 then -- start bit is set then save nalu header and body local nalu_hdr = bit.bor(bit.band(fu_idr, 0xE0), bit.band(fu_hdr, 0x1F)) write_to_file(stream_info, string.char(nalu_hdr) .. h264tvb(2):string(), true, timestamp, seq, end_of_nalu) else -- start bit not set, just write part of nalu body write_to_file(stream_info, h264tvb(2):string(), false, timestamp, seq, end_of_nalu) end end -- call this function if a packet contains h264 payload function my_h264_tap.packet(pinfo,tvb) if stream_infos == nil then -- not triggered by button event, so do nothing. return end local h264s = { f_h264() } -- using table because one packet may contains more than one RTP local rtps = { f_rtp() } local rtp_seqs = { f_rtp_seq() } local rtp_timestamps = { f_rtp_timestamp() } for i,h264_f in ipairs(h264s) do if h264_f.len < 2 then return end local h264 = h264_f.value -- is ByteArray local hdr_type = bit.band(h264:get_index(0), 0x1F) local stream_info = get_stream_info(pinfo) -- search the RTP timestamp and sequence of this H264 local timestamp = -1 local seq = -1 if drop_uncompleted_frame then for j,rtp_f in ipairs(rtps) do if h264_f.offset > rtp_f.offset and h264_f.offset - rtp_f.offset <= 16 and h264_f.offset+h264_f.len <= rtp_f.offset+rtp_f.len then seq = rtp_seqs[j].value timestamp = rtp_timestamps[j].value break end end end if hdr_type > 0 and hdr_type < 24 then -- Single NALU process_single_nalu(stream_info, h264, timestamp, seq) elseif hdr_type == 24 then -- STAP-A Single-time aggregation process_stap_a(stream_info, h264, timestamp, seq) elseif hdr_type == 28 then -- FU-A process_fu_a(stream_info, h264, timestamp, seq) else twappend("Error: unknown type=" .. hdr_type .. " ; we only know 1-23(Single NALU),24(STAP-A),28(FU-A)!") end end end -- close all open files local function close_all_files() if stream_infos then local no_streams = true for id,stream in pairs(stream_infos) do if stream and stream.file then if stream.frame_buffer then local p = stream.frame_buffer while p do sort_and_write(stream, p) p = p.next end stream.frame_buffer = nil stream.frame_buffer_size = 0 end stream.file:flush() stream.file:close() twappend("File [" .. stream.filename .. "] generated OK!\n") stream.file = nil no_streams = false end end if no_streams then twappend("Not found any H.264 over RTP streams!") end end end function my_h264_tap.reset() -- do nothing now end local function remove() my_h264_tap:remove() end tw:set_atclose(remove) local function export_h264(drop_frame) pgtw = ProgDlg.new("Export H264 to File Process", "Dumping H264 data to file...") first_run = true drop_uncompleted_frame = drop_frame stream_infos = {} -- first time it runs for counting h.264 packets and finding SPS and PPS retap_packets() first_run = false -- second time it runs for saving h264 data to target file. retap_packets() close_all_files() -- close progress window pgtw:close() stream_infos = nil end local function export_all() export_h264(false) end local function export_completed_frames() export_h264(true) end tw:add_button("Export All", export_all) tw:add_button("Export Completed Frames (Drop uncompleted frames)", export_completed_frames) end -- Find this feature in menu "Tools->"Export H264 to file [HQX's plugins]"" register_menu("Export H264 to file [HQX's plugins]", export_h264_to_file, MENU_TOOLS_UNSORTED) end