avcodec_decode_video2()解码视频后丢帧的问题解决

使用libav转码视频时发现一个问题:

使用下面这段代码解码视频时,视频尾巴上会丢掉几帧。

while(av_read_frame(ifmt_ctx,&packet) >= 0){
    ret = avcodec_decode_video2(video_dec_ctx, vframe, &got_frame, &packet);
    if (got_frame) {
        packet.pts = av_rescale_q(packet.pts,video_dec_st->time_base,video_enc_st->time_base);
        write_video_frame(ofmt_ctx,video_enc_st,vframe);
    }
}

这是因为源视频中PTS与DTS的不同造成的。

av_read_frame()按照PTS顺序读帧的时候,如果此帧需要参考后面的帧,那么此时avcodec_decode_video2()是没有能力解码此帧的,表现为got_frame返回0。

比如说遇上如下EFGH四帧:

ID : E F G H
KIND: I B P P
PTS : 1 2 3 4
DTS : 1 4 2 3

那么顺序读到F时,由于F需要参考G帧,而此时我们还没读到G帧,我们是没有解码F的能力的,got_frame就返回0了。如果我们对此事不做处理,那么我们就会丢掉一个帧(但丢掉的未必是F,因为av_read_frame()和avcodec_decode_video2()是1:1调用的)。

所以我们需要在while(av_read_frame())读完整个视频后,继续调用avcodec_decode_video2()把之前那些没有成功解码的帧都解出来。调用的次数就是之前got_frame返回0的次数。

按照上述思路变更代码为以下,成功找回丢失的帧。

int skipped_frame = 0;

while(av_read_frame(ifmt_ctx,&packet) >= 0){
    ret = avcodec_decode_video2(video_dec_ctx, vframe, &got_frame, &packet);
    if (got_frame) {
        packet.pts = av_rescale_q(packet.pts,video_dec_st->time_base,video_enc_st->time_base);
        write_video_frame(ofmt_ctx,video_enc_st,vframe);
    }
    else
    {
        skipped_frame++;
    }
}

for(int i=skipped_frame; i>0; i--)
{
    ret = avcodec_decode_video2(video_dec_ctx, vframe, &got_frame, &packet);
    if (got_frame) {
        packet.pts = av_rescale_q(packet.pts,video_dec_st->time_base,video_enc_st->time_base);
        write_video_frame(ofmt_ctx,video_enc_st,vframe);
    }
}

flash.net.NetStream.appendBytes()在chrome下直播爆音的问题解决

去年底和东东耿星一起,研究了一下Adobe家HDS的具体实现 OSMF。利用其中的一个核心方法 flash.net.NetStream.appendBytes()构建了我们自己的HTTP点直播播放框架。

但今年年初发现一个问题:新框架直播的时候有爆音。

经过一段时间的现象收集,问题总结出来是这样:
1、直播的时候,播放一段时间后出现爆音;
2、同时去听别的电脑,并不是每台电脑上都会出现爆音;
3、出现爆音的不同电脑之间,Mac的爆音现象比Windows明显;
4、出现爆音的同一台电脑上,Mac上Chrome的爆音现象比Safari明显,Windows上Chrome的爆音现象比IE明显;
5、如果不经过浏览器,本地打开swf来播放流,则无爆音。

首先,我们想到了浏览器问题。于是升级最新的浏览器和FP,无效。

然后,开始怀疑Ffmpeg的segment muxer,于是俺自己写了一个Flv segmenter,无效。

接着,尝试看看播放框架本身是不是有什么问题。

我们原有的逻辑是这样:每个flv片都自带flv头以及AVC和AAC的第0帧,每次调用appendBytes()都调用一次appendBytesAction(RESET_BEGIN)。

现在我先用gnu split将一个大的flv文件按照300k一个切开,这样就只有第一个文件片有flv头和AVC的AAC的第0帧了。再构造对应的假直播接口,然后将播放框架的改成只有下载到第一片的时候才调用flash.net.NetStream.appendBytesAction(RESET_BEGIN),而后所有的片都只调用flash.net.NetStream.appendBytes()。

此时我惊喜地发现,爆音现象解除了!

但出现了另一个问题,播放画面开始出现卡顿。于是继续查找卡顿的原因。

卡顿的原因很简单。appendBytes()方法自带了2个buffer。除开正常的IO buffer,还有一个FIFO的tag buffer。它最多存放一个flv tag,每拼装出一个完整的flv tag就将其推入IO buffer。这样它就可以负责保证虽然数据都是一片一片碎片拼装起来的,但IO buffer中都是完整的flv tag。

我之前使用gnu split切分出来的文件肯定不是按照tag的边缘切分的文件。经观察,在片间填充tag buffer的时候就会出现卡顿。

于是使用自己的flv segmenter严格按照tag边缘来切分文件。

卡顿消失!但爆音归来……

最后,我们就想到这爆音会不会跟buffer有点关系呢?

编写框架初期,我们为了让用户减少直播延迟,设置了bufferTimeMax为20,作用是让buffer太满的用户加速播放它们buffer中的流,以追上最新直播点。是不是这个机制导致的爆音呢?

查看OSMF,它是将bufferTimeMax写死为0的。于是我们也将bufferTimeMax设为0。

爆音消失!

反推出几个结论:
1、之前部分电脑无法再现爆音问题,很有可能是因为它们的网速没有快到可以填满buffer的地步;
2、我们可能要使用跳过一些片的方式让网速不稳定用户减少直播的延迟;
3、OSMF里面还是有很多营养值得去吸取的。

这是次历时月余的查错。

现在终于解除了纠结,这感觉真好,真好!

CPAN上几个我喜欢的HTTP Server模块

1. HTTP::Daemon

特点:
我的最爱;

易用度:
接口暴露地干净利落,嵌入使用的最佳选择;

优点:
代码风格帅气流畅,一气呵成。且各子模块分工明确,单独可用性高。

2. Net::Server::HTTP

特点:
1、CPAN上favorite投票得分最高的HTTP Server模块;
2、实现了类似Pyhton的”python -m SimpleHTTPServer“,
Perl的一行HTTP Server:”perl -e ‘use base qw(Net::Server::HTTP); main->run(port => 8080)’“;

易用度:
1、可能是这几个中最普遍应用在生产环境中的;
2、嵌入使用时使用了函数名约定的方式,感觉不大好;
3、很好地处理了和CGI模块的接驳;

优点:
1、默认采用了Fork模型,很简单就可以配置成其它模型;
2、测试集十分完整。

3. HTTP::Server::Encrypt

特点:
我写的;

易用度:
1、默认将HTTP请求路由到配置目录下的对应.pl文件,类mod_php的使用方式。
2、如果要当作模块嵌入使用的话,需要通过package继承的方式去做,很丑。

优点:
1、支持BODY体加密;
2、内置HTTP auth、IP黑白名单、sendfile等特性;
3、首次启动时,脚本全部编译后载入内存。性能超越httpd。(是的,脚本变更时需要重启)

4. HTTP::Server::Simple

特点:
很好的编程示例;

易用度:
1、更适合作为产品,而非模块使用;
2、嵌入使用时需要写自己的package继承的方式去做,很丑;

优点:
1、代码分层清晰;
2、可以十分简单地通过”net_server”参数配置进程模型。

介绍一个Perl杀死孙子进程的模块

如果子进程没有合理处理信号量,且代码又难以被我们改写(比如”bash -c”)。

那么作为主进程想要杀死孙子进程就是件特麻烦的事。

那么,介绍一个把这些麻烦封装掉的模块:
Proc::Killfam

另外,附录几个同样简单可靠的进程处理模块:
管理Fork:Proc::Fork
做Daemon:App::Daemon Proc::Daemon

用Perl开发Windows平台GUI的工具集

The IUP module is a cross-platform GUI toolkit designed to run on MS Windows (incl. Cygwin), GTK+ and Motif/X11. On all platform it uses native GUI widgets.

https://metacpan.org/module/KMX/IUP-0.100/lib/IUP.pod

Perl Win32::GUI文档

1、介绍:https://metacpan.org/module/ROBERTMAY/Win32-GUI-1.06/docs/GUI /Tutorial.pod
2、使用手册:http://perl-win32-gui.sourceforge.net/cgi-bin /docs.cgi?doc=reference-packages

免费Perl打包工具

1、Cava Packager
之前我用的。刚打开看了下,似乎改了不少。
介绍:http://www.cavapackager.com/appdocs/overview-quickstart.htm
下载:http://www.cavapackager.com/

2、pp
台湾的传奇 唐凤作品,许伦的最爱。

https://metacpan.org/module/pp

收费Perl 打包工具

1、BoxedApp

http://www.boxedapp.com/boxedapppacker/order.html#.UWIzM1t5HDc

2、Perl Dev Kit (PDK)

http://www.activestate.com/perl-dev-kit

附送一个小demo

use File::Basename qw(dirname basename);
use Data::Dump qw(ddx);
use Win32::GUI();
use Win32::Process;
use Win32;
use Win32::SystemInfo;
use POSIX qw(strftime);

$|++;

my %phash;
Win32::SystemInfo::ProcessorInfo(%phash);
my $thread_num = $phash{NumProcessors} * 2 + 1;
undef %phash;

my $rootdir = Win32::GetCwd();
chdir($rootdir);

my $tempdir       = $rootdir . '/tmp/';
my $libdir        = $rootdir . '/lib/';
my $confdir       = $rootdir . '/conf/';
my $errlogfile    = $rootdir . '/log/errlog.log';
my $inputdir      = readfile("$confdir/indir.conf") || $rootdir;
my $outputdir     = readfile("$confdir/outdir.conf") || $rootdir;
my $radio_checked = readfile("$confdir/radio.conf") || 'flv';

$MyIcon = new Win32::GUI::Icon("logo.ico");
$MyClass = new Win32::GUI::Class(-name => "my_Win32GUI_class_with_changed_icon",
                                 -icon => $MyIcon,);

my $W1 = Win32::GUI::Window->new(
                                 -name  => "W1",
                                 -title => "FLV文件分析小工具 v1.00",
                                 -pos   => [30, 30],
                                 -size  => [800, 600],
                                 -class => $MyClass,
                                );

my $inputfile_textfiled =
  $W1->AddTextfield(
                    -name   => 'Textfield1',
                    -text   => '',
                    -prompt => ["待分析FLV文件:", 90],
                    -pos    => [10, 20],
                    -size   => [320, 20],
                    -align  => 'left',
                   );
$W1->AddButton(-name => "Button1", -text => "浏览..", -pos => [435, 20],);

my $checkbox1 = $W1->AddCheckbox(
                                 -name    => "Checkbox1",
                                 -text    => "只看关键帧",
                                 -checked => 1,
                                 -pos     => [500, 20],
                                );

$W1->AddButton(
               -name => "Button3",
               -text => "开始分析",
               -pos  => [650, 20],
               -size => [110, 20]
              );

my $inputfile_textfiled2 =
  $W1->AddTextfield(
                    -name      => 'Textfield2',
                    -multiline => 1,
                    -readonly  => 1,
                    -text      => '',
                    -pos       => [10, 50],
                    -size      => [760, 500],
                    -align     => 'left',
                   );

$W1->Show();
Win32::GUI::Dialog();
exit(0);

sub Button1_Click
{
    my $file =
      Win32::GUI::GetOpenFileName(
                               -title     => "选择要分析的文件 ..",
                               -file      => "\0" . " " x 256,
                               -directory => $inputdir,
                               -filter => ["FLV video files" => "*.flv;*.hlv",],
      );
    $inputfile_textfiled->{'-text'} = $file;
    return 1;
}

sub Button3_Click
{
    my $input = $inputfile_textfiled->{'-text'};
    unless (-e $input and -f $input and -s $input)
    {
        Win32::MsgBox('请正确选择待转文件', MB_ICONINFORMATION, '错误');
        return 0;
    }

    unless ($input =~ /lv/)
    {
        Win32::MsgBox('请正确选择待转文件', MB_ICONINFORMATION, '错误');
        return 0;
    }

    my $file     = $input;
    my $filesize = -s $file;
    my $tpos     = 13;

    open FH, $file or die $!;
    seek FH, 13, 0 or die $!;

    my $result =
      "偏移量\t\tTAG类型\t\tTAG大小\t\t时间\t\t编码\t\t帧类型\t\t包类型\r\n\r\n";
    while ($tpos < $filesize)
    {
        my $buf;
        read FH, $buf, 1 or die $!;
        my ($type) = unpack("C", $buf);
        read FH, $buf, 3 or die $!;
        my ($size) = unpack("N", chr(0) . $buf);
        read FH, $buf, 3 or die $!;
        my ($time) = unpack("N", chr(0) . $buf);

        read FH, $buf, 1 or die $!;
        my ($timeext) = unpack("C", $buf);
        read FH, $buf, 3 or die $!;
        my ($streamid) = unpack("N", chr(0) . $buf);
        read FH, $buf, 1 or die $!;
        my ($byte) = unpack("C", $buf);
        my $codec = '-1';
        my $frametype = "-1";
        if ($type == 8)
        {
            $codec = $byte >> 4;
        }
        elsif ($type == 9)
        {
            $codec     = $byte & 0xffff;
            $frametype = $byte >> 4;
        }
        read FH, $buf, 1 or die $!;
        my ($ptype) = unpack("C", $buf);
        if ($type == 18)
        {
            $ptype = -1;
        }

        if ($type == 18)
        {
            $type = 'script';
        }
        elsif ($type == 8)
        {
            $type = 'audio';
        }
        elsif ($type == 9)
        {
            $type = 'video';
        }

        if ($frametype == 1)
        {
            $frametype = 'key';
        }
        else
        {
            $frametype = '';
        }

        if ($ptype == 0)
        {
            $ptype = 'codec header';
        }
        else
        {
            $ptype = '';
        }

        if ($checkbox1->Checked)
        {
            $result .=
              "$tpos\t\t$type\t\t$size\t\t$time\t\t$codec\t\t$frametype\t\t$ptype\r\n"
              if ($frametype eq 'key');
        }
        else
        {
            $result .=
              "$tpos\t\t$type\t\t$size\t\t$time\t\t$codec\t\t$frametype\t\t$ptype\r\n";
        }
        $tpos += $size + 4 + 11;
        seek FH, $tpos, 0 or die $!;
    }

    close FH;

    #Win32::MsgBox('文件分析完成', MB_ICONINFORMATION, '完成');
    $inputfile_textfiled2->{'-text'} = $result;
    return 1;
}

sub W1_Terminate
{
    return -1;
}

sub process
{
    my $exefile = shift;
    my $cmd     = shift;
    my $process;
    my $exitcode;
    Win32::Process::Create($process, $exefile, $cmd, 0, NORMAL_PRIORITY_CLASS,
                           ".");
    $process->Suspend();
    $process->Resume();
    $process->Wait(INFINITE);
    $process->GetExitCode($exitcode);

    if ($exitcode != 0)
    {
        logerr($^E . $exefile . $cmd);
    }

    return $exitcode;    #exitcode=0 means right, 1 means wrong.
}

sub logerr
{
    my $message = shift;
    $message =
      strftime("%Y-%m-%d %H:%M:%S", localtime) . "\t" . $message . "\n";
    open my $fh, '>>', $errlogfile;
    print $fh $message;
    close $fh;
    return 1;
}

sub readfile
{
    my $file = shift or return 0;
    local $/;
    open my $fh, '<', $file;
    my $content = <$fh>;
    close $fh;
    return $content;
}

sub writefile
{
    my $file = shift or return 0;
    my $content = shift;
    open my $fh, '>', $file;
    print $fh $content;
    close $fh;
    return 1;
}

sub tailfile
{
    my $file = shift or return 0;
    my $content = "";

    open(F, $file) or die $!;
    seek(F, 0, 2);    # set handler at the end of $file
    until ($content =~ m/\n(.*)\n?$/)
    {
        my $string;
        if (seek(F, -1024, 1))    # backward 1024 bytes
        {
            my $n = read(F, $string, 1024) or die $!;
            $content = $string . $content;
            last if ($n < 1024);
            seek(F, -1024, 1);
        }
        else
        {
            my $len = tell F;
            seek(F, 0, 0) || die "see error at $file";
            read(F, $string, $len) or die $!;
            $content = $string . $content;
            last;
        }
    }
    close(F);

    if ($content =~ m/\n\n$/)
    {
        print "\n";
    }
    elsif ($content =~ m/\n?(.*)\n?$/)
    {
        print "$1\n";
    }
    else
    {
        print $content, "\n";
    }
}


用Perl读二进制数据的小笔记

ord():
把读到内存的东西,当做无符号的数字或者字符(ACSII的)弄出来。

chr():
ord的反。chr(65)=”A”。

oct():
8进制 转 10进制

hex():
16进制 转 10进制

UI8 无符号整型 => 变量:

sysread(FH, $buf, 1);
my $var = unpack("C",$buf);

UI32 无符号整型 => 变量:

sysread(FH, $buf, 4);
my $var = unpack("N",$buf);

UI24 无符号整型 => 变量:

sysread(FH, $buf, 3);
my $data_size;
my @datasize;
($datasize[0],  $datasize[1],  $datasize[2]) = unpack 'CCC', $buf;
$data_size = ($datasize[0] * 256 + $datasize[1]) * 256 + $datasize[2];

如果一个byte中顺序包含UB[4] + UB[2] + UB[1] + UB[1]四个数据,那么以下:

sysread(FH, $buf, 1);
my $buf   = unpack 'C', $buf;
my $ub4   = (($buf >> 4) & 0x0f);
my $ub2   = (($buf >> 2) & 0x03);
my $ub1_1 = (($buf >> 1) & 0x01);
my $ub1_2 = $buf & 0x01;

如果一个byte中顺序包含UB[4] + UB[4]两个数据,那么以下:

sysread(FH, $buf, 1);
my $buf   = unpack 'C', $buf;
my $ub4_1 = ( $buf >> 4 ) & 0x0f;
my $ub4_2 = $buf & 0x0f;

MetaCPAN上最受欢迎的十大模块

MetaCPAN有Like按钮,而且这些Like都是CPAN作者们点击提交的,应该蛮能代表模块的优秀和受欢迎程度的。

但居然MetaCPAN没有按照Like数排序的功能,真是蛋疼。

于是我写了一个按照MetaCPAN中Like数排序搜索结果的小工具——CPAN搜索Like排序工具

主要是方便自己,也希望能方便到别人。

以下是目前MetaCPAN上Like数最高的十大模块。

Rank Likes Module
1 126 Moose
2 126 Class::MOP
3 113 Mojolicious
4 100 AnyDBM_File
5 100 English
6 100 Errno
7 100 ExtUtils::Embed
8 100 Getopt::Std
9 100 Hash::Util
10 100 Hash::Util::FieldHash

Moose高居第一,AnyEvent前50都没进,而POE和Coro更是掉出100开外。

嗯,看来高性能果然不是Perl用家最关心的地方。