ijkplayer基于rtsp直播延时的深度优化

2023-05-16

现在ijkPlayer是许多播放器、直播平台的首选,相信很多开发者都接触过ijkPlayer,无论是Android工程师还是iOS工程师。我曾经在Github上的ijkPlayer开源项目上提问过:视频流为1080P、30fps,如何优化RTSP直播的延时为大约100ms呢?发现大家对RTSP直播延时优化非常感兴趣,纷纷提问或者给出自己的观点。本文主要是总结,也是与大家探讨RTSP直播的延时优化。

 

目录

一、修改编译脚本支持RTSP

二、修改播放器的option参数

三、网络抖动的丢包

四、解码器设为零延时

五、减少FFmpeg拆帧等待延时

1、找到当前帧结束符

2、去掉parse_packet的while循环

3、 修改av_parser_parse2的帧偏移量

 4、去掉parser_parse的寻找帧起始码

5、修改parser.c的组帧方法


一、修改编译脚本支持RTSP

ijkPlayer默认是没有把RTSP协议编译进去,所以我们得修改编译脚本,原来的disable改为enable:

export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=rtp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-protocol=tcp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtsp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=sdp"
export COMMON_FF_CFG_FLAGS="$COMMON_FF_CFG_FLAGS --enable-demuxer=rtp"

二、修改播放器的option参数

//丢帧阈值
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 30);
//视频帧率
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "fps", 30);
//环路滤波
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48);
//设置无packet缓存
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
//不限制拉流缓存大小
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
//设置最大缓存数量
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max-buffer-size", 1024);
//设置最小解码帧数
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 3);
//启动预加载
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
//设置探测包数量
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probsize", "4096");
//设置分析流时长
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "2000000");

值得注意的是,ijkPlayer默认使用udp拉流,因为速度比较快。如果需要可靠且减少丢包,可以改为tcp协议:

mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "rtsp_transport", "tcp");

另外,可以这样开启硬解码,如果打开硬解码失败,再自动切换到软解码:

mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-auto-rotate", 0);
mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "mediacodec-handle-resolution-change", 0);

三、网络抖动的丢包

在拉流时,音频流、视频流是单独保存到缓冲队列的。如果发生网络抖动,就会引起缓冲抖动(JitBuffer),可以总结为网络卡顿导致音视频缓冲队列增大,从而导致解码滞后、播放滞后。此时,我们需要主动丢包来跟进当前时间戳。因为音视频同步一般以音频时钟为基准,人们对音频更加敏感,所以我们优先丢掉视频队列的包。但是,丢视频数据包时,需要丢掉整个GOP的数据包,因为B帧、P帧依赖I帧来解码,否则会引起花屏。有一位开发者叫做暴走大牙,他的一篇关于ijkPlayer直播延时的文章写得很好:ijkplay播放直播流延时控制小结

四、解码器设为零延时

大家应该听过编码器的零延时(zerolatency),但可能没听过解码器零延时。其实解码器内部默认会缓存几帧数据,用于后续关联帧的解码,大概是3-5帧。经过反复测试,发现解码器的缓存帧会带来100多ms延时。也就是说,假如能够去掉缓存帧,就可以减少100多ms的延时。而在avcodec.h文件的AVCodecContext结构体有一个参数(flags)用来设置解码器延时:

typedef struct AVCodecContext {
......
int flags;
......
}

为了去掉解码器缓存帧,我们可以把flags设置为CODEC_FLAG_LOW_DELAY。在初始化解码器时进行设置:

//set decoder as low deday
codec_ctx->flags |= CODEC_FLAG_LOW_DELAY;

五、减少FFmpeg拆帧等待延时

FFmpeg拆帧是根据下一帧的起始码来作为当前帧结束符,起始码一般是:0x00 0x00 0x00 0x01或者0x00 0x00 0x01。这样就会带来一帧的延时,这一帧延时能不能去掉呢?如果有帧结束符,我们以帧结束符来拆帧,这样做就能解决一帧延时。现在,问题变成找到帧结束符,然后替换成下一帧起始码来拆帧。整个调用流程是:read_frame—>read_frame_internal—>parse_packet—>av_parser_parse2—>parser_parse—>ff_combine_frame. 流程图如下:

1、找到当前帧结束符

在rtpdec.c文件的rtp_parse_packet_internal方法里,有获取帧结束符,也就是mark标志位,我们在这里设一个全局变量:

static int rtp_parse_packet_internal(RTPDemuxContext *s, AVPacket *pkt,
                                     const uint8_t *buf, int len)
{
    ......

    if (buf[1] & 0x80)
        flags |= RTP_FLAG_MARKER;
    //the end of a frame
    mark_flag = flags;

    ......
}

2、去掉parse_packet的while循环

我们在外部调用libavformat模块的utils.c文件的read_frame读取一帧数据,而read_frame调用内部方法read_frame_internal,read_frame_internal接着调用parse_packet方法,在该方法里有一个while循环体。现在把循环体去掉,并且释放申请的内存:

static int parse_packet(AVFormatContext *s, AVPacket *pkt, int stream_index)
{
    ......

//    while (size > 0 || (pkt == &flush_pkt && got_output)) {
        int len;
        int64_t next_pts = pkt->pts;
        int64_t next_dts = pkt->dts;

        av_init_packet(&out_pkt);
        len = av_parser_parse2(st->parser, st->internal->avctx,
                               &out_pkt.data, &out_pkt.size, data, size,
                               pkt->pts, pkt->dts, pkt->pos);
        pkt->pts = pkt->dts = AV_NOPTS_VALUE;
        pkt->pos = -1;
        /* increment read pointer */
        data += len;
        size -= len;

        got_output = !!out_pkt.size;

        if (!out_pkt.size){
            av_packet_unref(&out_pkt);//release current packet
            av_packet_unref(pkt);//release current packet
            return 0;
//            continue;
        }
    ......        
   
        ret = add_to_pktbuf(&s->internal->parse_queue, &out_pkt,
                            &s->internal->parse_queue_end, 1);
        av_packet_unref(&out_pkt);
        if (ret < 0)
            goto fail;
//    }

    /* end of the stream => close and free the parser */
    if (pkt == &flush_pkt) {
        av_parser_close(st->parser);
        st->parser = NULL;
    }

fail:
    av_packet_unref(pkt);
    return ret;
}

3、 修改av_parser_parse2的帧偏移量

在libavcodec模块的parser.c文件中,parse_packet调用到av_parser_parse2来解释数据包,该方法内部有记录帧偏移量。原先是等待下一帧的起始码,现在改为当前帧结束符,所以要把下一帧起始码这个偏移量长度去掉:

int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx,
                     uint8_t **poutbuf, int *poutbuf_size,
                     const uint8_t *buf, int buf_size,
                     int64_t pts, int64_t dts, int64_t pos)
{
    ......

    /* WARNING: the returned index can be negative */
    index = s->parser->parser_parse(s, avctx, (const uint8_t **) poutbuf,
                                    poutbuf_size, buf, buf_size);
    av_assert0(index > -0x20000000); // The API does not allow returning AVERROR codes
#define FILL(name) if(s->name > 0 && avctx->name <= 0) avctx->name = s->name
    if (avctx->codec_type == AVMEDIA_TYPE_VIDEO) {
        FILL(field_order);
    }

    /* update the file pointer */
    if (*poutbuf_size) {
        /* fill the data for the current frame */
        s->frame_offset = s->next_frame_offset;

        /* offset of the next frame */
//        s->next_frame_offset = s->cur_offset + index;
        //video frame don't plus index
        if (avctx->codec_type == AVMEDIA_TYPE_VIDEO) {
            s->next_frame_offset = s->cur_offset;
        }else{
            s->next_frame_offset = s->cur_offset + index;
        }
        s->fetch_timestamp   = 1;
    }
    if (index < 0)
        index = 0;
    s->cur_offset += index;
    return index;
}

 4、去掉parser_parse的寻找帧起始码

av_parser_parse2调用到parser_parse方法,而我们这里使用的是h264解码,所以在libavcodec模块的h264_parser.c有一个结构体ff_h264_parser,把h264_parse赋值给parser_parse:

AVCodecParser ff_h264_parser = {
    .codec_ids      = { AV_CODEC_ID_H264 },
    .priv_data_size = sizeof(H264ParseContext),
    .parser_init    = init,
    .parser_parse   = h264_parse,
    .parser_close   = h264_close,
    .split          = h264_split,
};

现在我们需要h264_parser.c文件的h264_parse方法,去掉寻找下一帧起始码作为当前帧结束符的过程:

static int h264_parse(AVCodecParserContext *s,
                      AVCodecContext *avctx,
                      const uint8_t **poutbuf, int *poutbuf_size,
                      const uint8_t *buf, int buf_size)
{
    ......

    if (s->flags & PARSER_FLAG_COMPLETE_FRAMES) {
        next = buf_size;
    } else {
//TODO:don't use next frame start code, modify by xufulong
//        next = h264_find_frame_end(p, buf, buf_size, avctx);

        if (ff_combine_frame(pc, next, &buf, &buf_size) < 0) {
            *poutbuf      = NULL;
            *poutbuf_size = 0;
            return buf_size;
        }

/*        if (next < 0 && next != END_NOT_FOUND) {
            av_assert1(pc->last_index + next >= 0);
            h264_find_frame_end(p, &pc->buffer[pc->last_index + next], -next, avctx); // update state
        }*/
    }

    ......
}

5、修改parser.c的组帧方法

h264_parse又调用parser.c的ff_combine_frame组帧方法,我们在这里把mark替换起始码作为帧结束符:

external int mark_flag;//引用全局变量

int ff_combine_frame(ParseContext *pc, int next,const uint8_t **buf, int *buf_size)
{
    ......

    /* copy into buffer end return */
//    if (next == END_NOT_FOUND) {
        void *new_buffer = av_fast_realloc(pc->buffer, &pc->buffer_size,
                                           *buf_size + pc->index +
                                           AV_INPUT_BUFFER_PADDING_SIZE);

        if (!new_buffer) {
          
            pc->index = 0;
            return AVERROR(ENOMEM);
        }
        pc->buffer = new_buffer;
        memcpy(&pc->buffer[pc->index], *buf, *buf_size);
        pc->index += *buf_size;
//        return -1;
          if(!mark_flag)
            return -1;
        next = 0;
//    }

    ......

}

经过以上修改,局域网用电脑推送1080P、30fps的视频流,Android设备拉流解码播放,整体延时可优化至130ms左右。而手机推流,延时可达到86ms。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ijkplayer基于rtsp直播延时的深度优化 的相关文章

  • c++简单实现http协议服务器和客户端

    C 43 43 简单实现HTTP GET POST 请求 HTTP 超文本传输协议 是一种客户端与服务端的传输协议 xff0c 最早用于浏览器和服务器之间的通信 xff0c 后来因为其使用灵活 方便等特点 xff0c 广泛用于客户端与服务端
  • QT中设置背景颜色

    layout大小设置方法 xff1a reSize QSize 600 600 就ok了 xff0c 这个是设置主窗口的大小 xff0c layout会自动适应主窗口的 QWidget是所有用户界面对象的基类 xff0c 这意味着可以用同样
  • vs2013 Warning 44 warning LNK4099: PDB 'vc120.pdb' was not found with '

    Warning 20 warning LNK4099 PDB 39 vc120 pdb 39 was not found with 39 ABC lib XYZ obj 39 or at 39 E test Release vc120 pd
  • QT信号与槽的6种连接方式以及自定义参数传递

    前言 一 信号与槽的连接 二 connect的第五个参数 三 传递参数为自定义参数时 扩展 前言 QT提供了信号与槽机制来实现对象之间的通信 xff0c 只有QObject及其派生类才能使用信号和槽机制 xff0c 且在类之中还需要使用Q
  • QT中QThread的各个方法,UI线程关系,事件关系详解(2)

    QThread 的两种使用方法 1 不使用事件循环 这是官方的 Manual example 以及相关书籍中都介绍的一种的方法 a 子类化 QThread b 重载 run 函数 xff0c run函数内有一个 while 或 for 的死
  • QT或MFC中调用Opencv需要引用库时或自身的架构库时可以添加环境变量引用路径下文件的方式搭建环境避免可执行程序下文件过多显得臃肿

    在计算机系统环境变量中在Path里添加要引用的lib dll等库文件
  • C# 中delegate、event、Action、Func详解

    都属于委托 xff0c 只是展现的形式不同而已 xff0c 无论哪种 xff0c 其实都可以采用delegate实现 xff0c 为什么会出现另外的三种呢 xff1f 因为delegate是很宽泛的 xff0c 格式内容都不受限 xff0c
  • (吐了呀,相同代码,相同case测试结果不一样)1052 Linked List Sorting

    在刷PAT的过程中 xff0c 关于一个样例的疑问 include lt iostream gt include lt list gt include lt vector gt include lt unordered map gt inc
  • C# 委托,泛型委托,匿名委托,lambda表达式

    一 泛型的定义及作用 泛型 generic 是C 2 0推出的新语法 xff0c 它是专门为处理多段代码在不同的数据类型上执行相同的指令的情况而设计的 比如说编程时 xff0c 碰到功能非常相似的模块 xff0c 只是它们所处理的数据类型不
  • C#连接sqlServer数据库详解

    C 是如何跟SQL Server进行连接的 xff1f 在C NET程序设计中 xff0c 离不开ADO NET ADO NET是 NET连接数据库的重要组件 使用其可以很方便地访问数据库 xff0c ADO NET还可以访问Oracle数
  • C++ 如何用创建txt文件,并且写入内容(汇总)

    void CreatTxt char pathName unsigned char rBuffer int length 创建txt文件 char path 61 34 C 1 txt 34 你要创建文件的路径 ofstream fout
  • 常用邮箱的 IMAP/POP3/SMTP 设置

    通过网上查找的资料和自己的总结完成了下面的文章 xff0c 看完之后相信大家对这三种协议会有更深入的理解 如有错误的地方望指正 POP3 POP3是Post Office Protocol 3的简称 xff0c 即邮局协议的第3个版本 它规
  • Critical error detected c0000374

    最近发现一个新奇的情况导致这个问题出现 版本不一致 简单来说 xff0c 就是有一个类A xff0c 调用类B xff1b 但是这个类B有两个版本B1 xff0c B2 大小不一致 xff1b 类B包含两个类C D 在调用类B时 xff0c
  • 串口通信协议

    概念 串口通信 xff08 Serial Communications xff09 的概念非常简单 xff0c 串口按位 xff08 bit xff09 发送和接收字节 尽管比按字节 xff08 byte xff09 的并行通信慢 xff0
  • [二] Nuttx移植-星瞳pyboard开发板

    目录 一 Nuttx配置文件二 构建自己的配置文件1 include board h文件构建2 kernel amp amp scripts 构建3 nsh defconfig 构建4 src 构建5 Kconfig 构建 三 修改 nut
  • Parrot Bebop2 与ROS

    第二章 无人机平台与开发环境搭建 本章主要介绍无人机平台及相关开发环境的搭建 包括介绍Parrot Bebop2的相关规格与使用说明 xff0c 以及ROS的操作系统的简介 发展历程 安装流程 xff0c 还有ROS的数据通信方式和ROS的
  • python2与python3解析数据

    蓝牙模块接收到监测设备传输来的数据 xff0c 封装格式为十六进制的数据帧 xff0c 蓝牙模块将数据通过串口发送给wrtnode 2p xff0c wrtnode通过ser2net服务将数据转为网络数据 xff0c 可以通过监听192 1
  • 上传本地项目到github远程仓库

    前提已经注册github账号并在本地电脑安装git客户端 1 为Github账户设置SSH key 进入git bash xff0c 通过如下命令生成 ssh keygen t rsa C 34 github所绑定的邮箱 34 一路回车 x
  • 卫星导航定位技术二:由星历参数求解卫星时空位置

    卫星星历是描述卫星运动轨道的信息 也可以说卫星星历就是一组对应某一时刻的轨道参数及其变率 有了卫星星历就可以计算出任意时刻的卫星位置及其速度 GPS卫星星历分为预报星历和后处理星历 预报星历又称广播星历 GPS广播星历参数共有16个 xff
  • 模式识别:最小错误率贝叶斯决策分类

    一 引言 1 用贝叶斯决策理论分类要事先知道两个条件及要求 xff1a 各类的先验概率 xff1a 及特征向量的条件概率密度 xff1a 或后验概率 xff1a 决策分类的类别一定 2 解决的问题 xff1a 已知一定数目的样本 xff0c

随机推荐

  • 模式识别:BP神经网络算法

    1 BP神经网络分类器 1 1 BP算法基本原理 神经网络结构大概如下图1 1 xff1a 图1 1 包括输入层 xff0c 隐层和输出层 包含一层隐层的神经网络称为浅层神经网络 xff0c 即SNN 包含多层隐层的神经网络称为深度神经网络
  • 模式识别:C-means(K-means)聚类算法与分级聚类(层次聚类)算法

    C均值聚类算法与分级聚类算法的聚类分析 一 实验目的 理解聚类的整体思想 xff0c 了解聚类的一般方法 xff1b 掌握 C means与分级聚类算法算法思想及原理 xff0c 并能够熟练运用这些算法进行聚类分析 xff1b 能够分析二者
  • ROS 配置多网口通讯

    列出当前所有的网络设备 ifconfig a 结果如下 xff1a enp1s0 Link encap Ethernet HWaddr 00 2f 5c 68 06 ad inet addr 192 168 1 101 Bcast 192
  • qt creator开启openMP加速方法

    环境 Qt creator4 11 for msvc2017 内置openmp库 启用方法 1 在pro文件加上QMAKE CXXFLAGS 43 61 openmp 2 添加头文件omp h
  • c++中::的用法

    是运算符中等级最高的 xff0c 它分为三种 1 global scope 全局作用域符 xff09 xff0c 用法 xff08 name 2 class scope 类作用域符 xff09 xff0c 用法 class name 3 n
  • 【ubuntu】——gflags&glog卸载与安装

    gflags glog 通过apt安装的glog xff0c gflags没有config cmake xff0c 所以在一些情况下需要手动编译 1 卸载gflags amp glog 只适用于通过apt安装的方式 span class t
  • 【算法】A* 寻路 可视化

    如下图 寻路图A 使用A 算法 xff0c 需要将地图抽象成一个个方块 xff0c 蓝色代表不可以动 墙 xff0c 黄色为起始点 xff0c 红色为目标点 其地图的二维坐标如图所示 xff0c 每一个单位为1米 A 的基本公式为 F n
  • 实验室新生成长指南[2.2.1] · 连接器

    欢迎进入 实验室新生成长指南 第二章 xff1a 硬件 本篇是 实验室新生成长指南 第二章第二节第一篇 xff1a 连接器 整个2 2节将帮助新手快速建立设计电路系统的一些基本知识储备 更多关于 实验室新生成长指南 的介绍 xff0c 请前
  • 走进音视频的世界——音视频的基本概念

    音视频通用的基本概念有码率 时长 xff0c 而不同音视频有不同的封装格式 编码协议 其中视频关键参数有分辨率 帧率 画质 旋转角度 像素格式 xff0c 而音频关键参数有采样率 声道数 声道布局 音质 采样数 采样位数 帧时长 接下来与大
  • 走进音视频的世界——新一代开源编解码器AV1

    AOMedia Video 1 xff08 AV1 xff09 是一种开源 免版税的编解码器 xff0c 最初设计用于Internet上的视频传输 它是由开放媒体联盟 xff08 AOMedia xff09 于VP9的继任者开发的 xff0
  • FFmpeg源码分析:avformat_find_stream_info分析码流信息

    FFmpeg在调用avformat open input 之后 xff0c 可能码流信息不够完整 xff0c 可以使用avformat find stream info 获取更多的码流信息 比如获取视频帧率 视频宽高 xff0c 重新计算最
  • Miracast投屏协议深入剖析

    Miracast由WiFi联盟制定 xff0c 以WiFi Direct IEEE802 11为无线传输标准 xff0c 允许手机向电视或其他接收设备进行无线投送视频 图片 和Miracast类似的投屏协议 xff0c 还有Airplay
  • c++:DFS与BFS详解

    DFS xff08 深度优先搜索 xff09 xff1a 从某个状态开始 xff0c 不断转移状态到无法转移为止 xff0c 然后退回到前一步 xff0c 继续转移到其他状态 xff0c 不断重复 xff0c 直至找到最终的解 总是从最开始
  • 一文掌握OpenGL的shader内置函数

    OpenGL ES有大量的GLSL内置函数 xff0c 包括 xff1a 三角函数 指数函数 通用函数 浮点函数 几何函数 矩阵函数 矢量关系函数 纹理函数 原子函数 图像函数 插值函数等 目录 一 三角函数 1 radians degre
  • 安全可靠的SRT实时传输协议

    Secure Reliable Transport SRT 是安全 可靠 低延时的多媒体实时传输协议 SRT协议使用AES进行数据加密 xff0c 运用FEC进行前向纠错 xff0c 并且有流量控制 拥塞控制 类似于QUIC协议 xff0c
  • android端使用openCV实现车牌检测

    现在 xff0c 汽车的踪影无处不在 xff0c 公路上疾驰 xff0c 大街边临停 xff0c 小区中停靠 xff0c 车库里停泊 管理监控如此庞大数量的汽车是个头疼的问题 精明的人们把目光放在车牌上 xff0c 因为车牌是汽车的 身份证
  • android端使用openCV与深度学习实现车牌识别

    车牌识别的应用场景随处可见 xff1a 高速公路上超速抓拍 小区门口关卡 车库入口关卡 xff0c 甚至出现在车载设备上 它的工作原理大致这样 xff1a 使用摄像头充当 眼睛 xff0c 使用openCV与深度学习充当 大脑 实时车牌识别
  • FFmpeg音频处理——音频混合、拼接、剪切、转码

    接触FFmpeg有一段时间了 xff0c 它是音视频开发的开源库 xff0c 几乎其他所有播放器 直播平台都基于FFmpeg进行二次开发 本篇文章来总结下采用FFmpeg进行音频处理 xff1a 音频混合 音频剪切 音频拼接与音频转码 采用
  • Android三种方式截取任意界面屏幕

    一 使用MediaProjectionManager Android5 0之后 xff0c 开放截取屏幕的API xff0c 也就是利用MediaProjectionManager创建VirtualDisplay xff0c 传入与Imag
  • ijkplayer基于rtsp直播延时的深度优化

    现在ijkPlayer是许多播放器 直播平台的首选 xff0c 相信很多开发者都接触过ijkPlayer xff0c 无论是Android工程师还是iOS工程师 我曾经在Github上的ijkPlayer开源项目上提问过 xff1a 视频流