首页 > 编程语言 >zlmediakit源码学习(扩展支持定时抽帧)

zlmediakit源码学习(扩展支持定时抽帧)

时间:2023-08-11 17:34:16浏览次数:40  
标签:zlmediakit string frame ctx ret 抽帧 源码 path const

使用了很长时间的zlmediakit流媒体服务,一直对其精妙高效的设计实现十分好奇。最好的学习就是去二次开发实现一些小功能,同时摸索框架的代码结构

在参考了zlmediakit的录像功能后,分析模仿它的源码结构,实现定时抽帧的功能。

抽帧之后可以:1)进行算法分析;2)重新编码实现转码功能;3)算法分析之后再编码,实现算法结果视频流程序。

向优秀的流媒体服务zlmediakit致敬!!

--------------------------------

关键代码如下:

1.在installWebApi方法中新增开始转码和停止转码的HTTP接口
void installWebApi() {  
    *****
    api_regist("/index/api/startTranscode", [](API_ARGS_MAP_ASYNC) {
        CHECK_SECRET();
        CHECK_ARGS("type", "vhost", "app", "stream");
        auto src = MediaSource::find(allArgs["vhost"], allArgs["app"],  allArgs["stream"]);
        if (!src) {
            throw ApiRetException("can not find the stream", API::NotFound);
        }
        src->getOwnerPoller()->async([=]() mutable {
            auto result =  src->setupTranscode((Transcoder::type)allArgs["type"].as<int>(), true,  allArgs["customized_path"],
                allArgs["max_second"].as<size_t>());
            val["result"] = result;
            val["code"] = result ? API::Success : API::OtherFailed;
            val["msg"] = result ? "success" : "start record failed";
            invoker(200, headerOut, val.toStyledString());
        });
    });
    ******
    api_regist("/index/api/stopTranscode", [](API_ARGS_MAP_ASYNC) {
        CHECK_SECRET();
        CHECK_ARGS("type", "vhost", "app", "stream");
        auto src = MediaSource::find(allArgs["vhost"], allArgs["app"],  allArgs["stream"]);
        if (!src) {
            throw ApiRetException("can not find the stream", API::NotFound);
        }
        src->getOwnerPoller()->async([=]() mutable {
            auto result = src->setupTranscode(
                (Transcoder::type)allArgs["type"].as<int>(), false,  allArgs["customized_path"],
                allArgs["max_second"].as<size_t>());
            val["result"] = result;
            val["code"] = result ? API::Success : API::OtherFailed;
            val["msg"] = result ? "success" : "start record failed";
            invoker(200, headerOut, val.toStyledString());
        });
    });
    *****
}
2.在MediaSource、MediaSourceEvent、MediaSourceEventInterceptor、MultiMediaSourceMuxer中模仿setupRecord添加setupTranscode     调用顺序是:MediaSource::setupTranscode ---> MediaSourceEventInterceptor::setupTranscode  ---> MultiMediaSourceMuxer::setupTranscode     最终执行创建转码的对象,并赋值给MultiMediaSourceMuxer::_transcode
bool MediaSource::setupTranscode(Transcoder::type type, bool start, const string  &custom_path, size_t max_second) {
    auto listener = _listener.lock();
    if (!listener) {
        WarnL << "未设置MediaSource的事件监听者,setupRecord失败:" << getSchema() <<  "/" << getVhost() << "/"
              << getApp() << "/" << getId();
        return false;
    }
    return listener->setupTranscode(*this, type, start, custom_path, max_second);
}

bool MediaSourceEventInterceptor::setupTranscode(MediaSource &sender,  Transcoder::type type, bool start, const string &custom_path, size_t max_second) {
    auto listener = _listener.lock();
    if (!listener) {
        return false;
    }
    return listener->setupTranscode(sender, type, start, custom_path, max_second);
}

bool MultiMediaSourceMuxer::setupTranscode(MediaSource &sender, Transcoder::type  type, bool start, const std::string &custom_path, size_t max_second) {
    if (start && !_transcode) {
        //开始转码
        _transcode = makeTranscoder(sender, getTracks(), Transcoder::type_mp4,  custom_path, max_second);    //创建转码对象
    } else if (!start && _transcode) {
        //停止转码
        _transcode = nullptr;
    }
    return true;
}
3.在zlmediakit项目中模拟Record,创建Transcode相关的对象:Transcoder、FFmpegTranscoder、FFmpegMuxer 4.Transcoder::getTranscodePath。用于获取抽帧截图的文件夹目录
std::string Transcoder::getTranscodePath( type type, const std::string &vhost,  const std::string &app, const std::string &stream_id, const std::string  &customized_path) {
    GET_CONFIG(bool, enableVhost, General::kEnableVhost);
    GET_CONFIG(string, recordPath, Record::kFilePath);
    GET_CONFIG(string, recordAppName, Record::kAppName);
    string mp4FilePath;
    if (enableVhost) {
        mp4FilePath = vhost + "/" + recordAppName + "/" + app + "/" + stream_id +  "/";
    } else {
        mp4FilePath = recordAppName + "/" + app + "/" + stream_id + "/";
    }
    // Here we use the customized file path.
    if (!customized_path.empty()) {
        return File::absolutePath(mp4FilePath, customized_path);
    }
    return File::absolutePath(mp4FilePath, recordPath);
}
5.Transcoder::createTranscoder。用于创建一个FFmpegTranscoder转码对象
std::shared_ptr<MediaSinkInterface> Transcoder::createTranscoder(type type, const  std::string &vhost, const std::string &app, const std::string &stream_id,const  std::string &customized_path, size_t max_second) {
    auto path = Transcoder::getTranscodePath(type, vhost, app, stream_id,  customized_path);
    return std::make_shared<FFmpegTranscoder>(path, vhost, app, stream_id,  max_second);
}
6.FFmpegTranscoder::FFmpegTranscoder。继承自MediaSinkInterface,可以作为一个输出类型的MediaSink。保存info信息,并创建一个FFmpegMuxer封装器对象
FFmpegTranscoder::FFmpegTranscoder(const std::string &path, const std::string  &vhost, const std::string &app, const std::string &stream_id,size_t max_second) {
    _folder_path = path;    _info.app = app;
    _info.stream = stream_id;
    _info.vhost = vhost;
    _info.folder = path;
    GET_CONFIG(size_t, recordSec, Record::kFileSecond);
    _max_second = max_second ? max_second : recordSec;
    _muxer = std::make_shared<FFmpegMuxer>(_folder_path, _max_second);
}
7.FFmpegTranscoder::addTrack。继承自MediaSink::addTrack。添加音视频轨道。最终是向封装器对象_muxer中添加轨道,暂时只关注视频轨道
bool FFmpegTranscoder::addTrack(const Track::Ptr &track) {
    _tracks.emplace_back(track);
    if (track->getTrackType() == TrackVideo) {
        _have_video = true;
        _muxer->addTrack(track);
        });
    }
    return true;
}
8.FFmpegTranscoder::inputFrame。继承自MediaSink::inputFrame,接收帧数据。将帧数据写入到了_muxer
bool FFmpegTranscoder::inputFrame(const Frame::Ptr &frame) {
    if (_muxer) {
        return _muxer->inputFrame(frame);
    }
    return true;
}
9.FFmpegMuxer::addTrack。创建视频解码器,用的是zlmediakit已经封装好的FFmpegDecoder,是一个基于ffmpeg的多线程异步解码对象,并且可以硬件解码器。     设置解码回调:解析将解码后的YUV数据帧转成图片格式并落地保存。
bool FFmpegMuxer::addTrack(const Track::Ptr &track) {
    if (track->getCodecId() == CodecH264) {
        _video_dec.reset(new FFmpegDecoder(track));
    } else if (track->getCodecId() == CodecH265) {
        _video_dec.reset(new FFmpegDecoder(track));
    } else {
    }
    if (_video_dec != nullptr) {
        _video_dec->setOnDecode([this](const FFmpegFrame::Ptr &frame) {
            time_t now = ::time(NULL);
            if (now - _last_time >= _gapTime) {
                AVFrame *avFrame = frame->get();
                int bufSize = av_image_get_buffer_size(AV_PIX_FMT_BGRA,  avFrame->width, avFrame->height, 64);
                uint8_t *buf = (uint8_t *)av_malloc(bufSize);
                int picSize = frameToImage(avFrame, AV_CODEC_ID_MJPEG, buf,  bufSize);
                if (picSize > 0) {
                    auto file_path = _folder_path + getTimeStr("%H-%M-%S_") +  std::to_string(_index) + ".jpeg";
                    auto f = fopen(file_path.c_str(), "wb+");
                    if (f) {
                        fwrite(buf, sizeof(uint8_t), bufSize, f);
                        fclose(f);
                    }
                }
                av_free(buf);
                _index++;
                _last_time = now;
            }
            
        });
    }
    return true;
}

10.FFmpegMuxer::frameToImage。利用FFmpeg将AVFrame的视频帧转成二进制数组输出

int FFmpegMuxer::frameToImage(AVFrame *frame, AVCodecID codecID, uint8_t *outbuf,  size_t outbufSize) {
    int ret = 0;
    AVPacket pkt;
    AVCodec *codec;
    AVCodecContext *ctx = NULL;
    AVFrame *rgbFrame = NULL;
    uint8_t *buffer = NULL;
    struct SwsContext *swsContext = NULL;
    av_init_packet(&pkt);
    codec = avcodec_find_encoder(codecID);
    if (!codec) {
        goto end;
    }
    if (!codec->pix_fmts) {
        goto end;
    }
    ctx = avcodec_alloc_context3(codec);
    ctx->bit_rate = 3000000;
    ctx->width = frame->width;
    ctx->height = frame->height;
    ctx->time_base.num = 1;
    ctx->time_base.den = 25;
    ctx->gop_size = 10;
    ctx->max_b_frames = 0;
    ctx->thread_count = 1;
    ctx->pix_fmt = *codec->pix_fmts;
    ret = avcodec_open2(ctx, codec, NULL);
    if (ret < 0) {
        printf("avcodec_open2 error %d", ret);
        goto end;
    }
    if (frame->format != ctx->pix_fmt) {
        rgbFrame = av_frame_alloc();
        if (rgbFrame == NULL) {
            printf("av_frame_alloc  fail");
            goto end;
        }
        swsContext = sws_getContext(
            frame->width, frame->height, (enum AVPixelFormat)frame->format,  frame->width, frame->height, ctx->pix_fmt,
            1, NULL, NULL, NULL);
        if (!swsContext) {
            printf("sws_getContext  fail");
            goto end;
        }
        int bufferSize = av_image_get_buffer_size(ctx->pix_fmt, frame->width,  frame->height, 1) * 2;
        buffer = (unsigned char *)av_malloc(bufferSize);
        if (buffer == NULL) {
            printf("buffer alloc fail:%d", bufferSize);
            goto end;
        }
        av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, buffer,  ctx->pix_fmt, frame->width, frame->height, 1);
        if ((ret = sws_scale(
                 swsContext, frame->data, frame->linesize, 0, frame->height,  rgbFrame->data, rgbFrame->linesize))
            < 0) {
            printf("sws_scale error %d", ret);
        }
        rgbFrame->format = ctx->pix_fmt;
        rgbFrame->width = ctx->width;
        rgbFrame->height = ctx->height;
        ret = avcodec_send_frame(ctx, rgbFrame);
    } else {
        ret = avcodec_send_frame(ctx, frame);
    }
    if (ret < 0) {
        printf("avcodec_send_frame error %d", ret);
        goto end;
    }
    ret = avcodec_receive_packet(ctx, &pkt);
    if (ret < 0) {
        printf("avcodec_receive_packet error %d", ret);
        goto end;
    }
    if (pkt.size > 0 && pkt.size <= outbufSize)
        memcpy(outbuf, pkt.data, pkt.size);
    ret = pkt.size;
end:
    if (swsContext) {
        sws_freeContext(swsContext);
    }
    if (rgbFrame) {
        av_frame_unref(rgbFrame);
        av_frame_free(&rgbFrame);
    }
    if (buffer) {
        av_free(buffer);
    }
    av_packet_unref(&pkt);
    if (ctx) {
        avcodec_close(ctx);
        avcodec_free_context(&ctx);
    }
    return ret;
}

 

11.FFmpegMuxer::inputFrame。向解码器中塞入H264或H265视频帧
bool FFmpegMuxer::inputFrame(const Frame::Ptr &frame) {
    if(frame->getTrackType() == TrackVideo && _video_dec != nullptr) {
        _video_dec->inputFrame(frame, true, false, false);
        if (_cb) {
            _cb(frame);
        }
    }
    return true;
}
12.MultiMediaSourceMuxer::onTrackFrame。MediaSource主对象收到视频帧时会向transcode对象中写入,从而实现抽帧
bool MultiMediaSourceMuxer::onTrackFrame(const Frame::Ptr &frame_in) {
    GET_CONFIG(bool, modify_stamp, General::kModifyStamp);
    auto frame = frame_in;
    if (modify_stamp) {
        //开启了时间戳覆盖
        frame = std::make_shared<FrameStamp>(frame,  _stamp[frame->getTrackType()],true);
    }
    bool ret = false;
    if (_rtmp) {
        ret = _rtmp->inputFrame(frame) ? true : ret;
    }
    if (_rtsp) {
        ret = _rtsp->inputFrame(frame) ? true : ret;
    }
    if (_ts) {
        ret = _ts->inputFrame(frame) ? true : ret;
    }
    //拷贝智能指针,目的是为了防止跨线程调用设置录像相关api导致的线程竞争问题
    //此处使用智能指针拷贝来确保线程安全,比互斥锁性能更优
    auto hls = _hls;
    if (hls) {
        ret = hls->inputFrame(frame) ? true : ret;
    }
    auto mp4 = _mp4;
    if (mp4) {
        ret = mp4->inputFrame(frame) ? true : ret;
    }
    auto transcode = _transcode;
    if (transcode) {
        ret = transcode->inputFrame(frame) ? true : ret;
    }
#if defined(ENABLE_MP4)
    if (_fmp4) {
        ret = _fmp4->inputFrame(frame) ? true : ret;
    }
#endif
#if defined(ENABLE_RTPPROXY)
    for (auto &pr : _rtp_sender) {
        ret = pr.second->inputFrame(frame) ? true : ret;
    }
#endif //ENABLE_RTPPROXY
    return ret;
}

标签:zlmediakit,string,frame,ctx,ret,抽帧,源码,path,const
From: https://www.cnblogs.com/feixiang-energy/p/17623567.html

相关文章

  • nginx源码分析之http解码实现
    分析nginx是如何解析并且存储http请求的。对非法甚至恶意请求的识别能力和处理方式。可以发现nginx采用状态机来解析http协议,有一定容错能力,但并不全面相关配置 跟解码有关的配置 merge_slashes 语法merge_slasheson|off默认值on上下文httpserver说明支持解析请求行时,合并相......
  • 如何在32位ubuntu11.10 下编译android 4.0.1源码和goldfish内核
    一准备工作 1安装javasdk6(1)从jdk官方网站http://www.oracle.com/technetwork/java/javase/downloads/jdk-6u29-download-513648.html下载jdk-6u29-linux-i586.bin文件。(2)执行jdk安装文件 [html] viewplaincopy1.$chmoda+xjdk-6u29-linux-i586.bin2.$jdk......
  • 直播源码连麦技术功能分享,你要的这里全有
    在直播源码的开发设计中,主播可以和观众进行连麦,可以给观众更直接的参与感,还能有利于提升直播平台用户活跃度和粘性。那么直播源码连麦技术是如何实现的呢?直播源码连麦功能流程图如下:一.需要连麦的观众发起连麦请求,进入连麦申请列表。二.主播从麦序中选择一名或多名观众进行连麦,从而......
  • 【Hystrix技术指南】(7)故障切换的运作流程原理分析(含源码)
    推荐超值课程:点击获取背景介绍目前对于一些非核心操作,如增减库存后保存操作日志发送异步消息时(具体业务流程),一旦出现MQ服务异常时,会导致接口响应超时,因此可以考虑对非核心操作引入服务降级、服务隔离。Hystrix说明官方文档Hystrix是Netflix开源的一个容灾框架,解决当外部依......
  • 智慧医疗PACS源码 C/S架构 自主研发
    C/S架构,即Client/Server(客户机/服务器)架构,将运算任务合理分配到客户机端和服务器端,降低了整个系统的通信开销,可以充分利用两端硬件环境的优势。C/S架构的PACS系统中,客户机(医学影像显示工作站)需要安装应用程序。才能查询数据、调取影像。C/S架构常用在局域网内,因此信息安全性更高,由......
  • RTSP/Onvif视频服务器LntonNVR(源码版)视频监控平台修改录像文件的存储位置的具体操作步
    LntonNVR是基于RTSP/Onvif协议接入的视频平台,具备视频直播监控、录像、检索与回看、存储、国标级联等视频能力,可支持将接入的视频流进行全平台、全终端的分发,包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等。在应用上,LntonNVR可以用在智慧工厂、智慧工地、智慧园区、智慧港口等......
  • 国标GB28181视频平台LntonGBS(源码版)国标平台新增拉流超时配置的具体操作流程
    LntonGBS是一款基于公安部推出的安防主流协议(国标GB28181协议)的视频接入、处理及分发平台。它提供了一系列功能,包括视频直播监控、云端录像、云存储、检索回放、智能告警、语音对讲和平台级联等。通过支持国标GB28181协议,LntonGBS能够实现与各种符合该协议的视频设备的连接和交互。......
  • 国标GB28181视频云服务平台LntonGBS(源码)国标平台对接宇视SDK,多次点击录像回放出现崩溃
    LntonGBS是一款基于国标GB28181协议的视频云服务平台。通过该平台,可以实现设备接入并支持视频的实时监控直播、录像、语音对讲、云存储、告警、级联等功能。此外,LntonGBS还支持将接入的视频流进行全终端、全平台的分发,包括支持RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流分发。另......
  • 【Eureka技术指南】「SpringCloud」从源码层面让你认识Eureka工作流程和运作机制(上)
    推荐超值课程:点击获取前言介绍了解到了SpringCloud,大家都应该知道注册中心,而对于我们从过去到现在,SpringCloud中用的最多的注册中心就是Eureka了,所以深入Eureka的原理和源码,接下来我们要进行讲解下eureka的源码分析,由此应运而产生的本章节的内容。基本原理EurekaServer提......
  • 【Eureka技术指南】「SpringCloud」从源码层面让你认识Eureka工作流程和运作机制(下)
    推荐超值课程:点击获取原理回顾EurekaServer提供服务注册服务,各个节点启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。EurekaClient是一个Java客户端,用于简化与EurekaServer......