SRS 代码分析【DVR录像实现】

DVR配置详解:https://github.com/ossrs/srs/wiki/v3_CN_DVR

推流到源站时会调用SrsOrginHub对象提供的initialize, on_publish, on_metadata, on_video, on_audio,on_unpublish函数。

1.SrsOriginHub::initialize初始化时会调用SrsDvr::initialize

srs_error_t SrsDvr::initialize(SrsOriginHub* h, SrsRequest* r)
{
    srs_error_t err = srs_success;
    
    req = r;
    hub = h;
    
    SrsConfDirective* conf = _srs_config->get_dvr_apply(r->vhost);
    actived = srs_config_apply_filter(conf, r);
    
    srs_freep(plan);
    if ((err = SrsDvrPlan::create_plan(r->vhost, &plan)) != srs_success) {
        return srs_error_wrap(err, "create plan");
    }
    
    std::string path = _srs_config->get_dvr_path(r->vhost);
    SrsDvrSegmenter* segmenter = NULL;
    if (srs_string_ends_with(path, ".mp4")) {
        segmenter = new SrsDvrMp4Segmenter();
    } else {
        segmenter = new SrsDvrFlvSegmenter();
    }
    
    if ((err = plan->initialize(hub, segmenter, r)) != srs_success) {
        return srs_error_wrap(err, "plan initialize");
    }
    
    return err;
}

首先调用SrvDvrPlan::create_plan根据配置文件来确定使用session(整个流录制成一个文件SrsDvrSessionPlan)还是segment(按照时长分段录制成多个文件SrsDvrSegmentPlan )的计划,这里只分析SrsDvrSessionPlan。

接着创建SrsDvrSegmenter,这里只分析flv文件录像SrsDvrFlvSegementer。

最后调用plan->initialize。SrsDvrSessionPlan直接使用基类的SrsDvrPlan::initialize;SrsDvrSegmentPlan重写了initialize方法在调用完SrsDvrPlan::initialize后接着从config文件中读取wait_keyframe, cduration配置参数。

SrsDvrPlan::initialize实现如下

srs_error_t SrsDvrPlan::initialize(SrsOriginHub* h, SrsDvrSegmenter* s, SrsRequest* r)
{
    srs_error_t err = srs_success;
    
    hub = h;
    req = r;
    segment = s;
    
    if ((err = segment->initialize(this, r)) != srs_success) {
        return srs_error_wrap(err, "segmenter");
    }
    
    if ((err = async->start()) != srs_success) {
        return srs_error_wrap(err, "async");
    }
    
    return err;
}

SrsDvrPlan::initialize首先调用SrsDvrSegmenter::initialize获取配置文件中的jitter_algorithm, wait_keyframe两个参数

接着调用async->start()开启协程,async是一个任务处理的协程。最终进入协程的循环处理中。

srs_error_t SrsAsyncCallWorker::cycle()
{
    srs_error_t err = srs_success;
    
    while (true) {
        if ((err = trd->pull()) != srs_success) {
            return srs_error_wrap(err, "async call worker");
        }
        
        if (tasks.empty()) {
            srs_cond_wait(wait);
        }
        
        std::vector copy = tasks;
        tasks.clear();
        
        std::vector::iterator it;
        for (it = copy.begin(); it != copy.end(); ++it) {
            ISrsAsyncCallTask* task = *it;
            
            int ret = ERROR_SUCCESS;
            if ((ret = task->call()) != ERROR_SUCCESS) {
                srs_warn("ignore async callback %s, ret=%d", task->to_string().c_str(), ret);
            }
            srs_freep(task);
        }
    }
    
    return err;
}

2.SrsOriginHub::on_publish发布流时会调用SrsDvr::on_publish()

int SrsDvr::on_publish()
{
    int ret = ERROR_SUCCESS;
    
    // the dvr for this stream is not actived.
    if (!actived) {
        return ret;
    }
    
    if ((ret = plan->on_publish()) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}
这里只分析SrsDvrSessionPlan的on_publish,代码如下

int SrsDvrSegmentPlan::on_publish()
{
    int ret = ERROR_SUCCESS;
    
    // support multiple publish.
    if (dvr_enabled) {
        return ret;
    }
    
    if (!_srs_config->get_dvr_enabled(req->vhost)) {
        return ret;
    }
    
    if ((ret = segment->close()) != ERROR_SUCCESS) {
        return ret;
    }
    
    if ((ret = segment->open()) != ERROR_SUCCESS) {
        return ret;
    }
    
    dvr_enabled = true;
    
    return ret;
}
调用close关闭已经打开的文件,接着生成文件路径,调用fragment->create_dir创建路径,最后调用fs->open打开一个包含临时的flv tmp文件。

int SrsDvrSegmenter::open()
{
    int ret = ERROR_SUCCESS;
    
    // ignore when already open.
    if (fs->is_open()) {
        return ret;
    }
    
    string path = generate_path();
    if (srs_path_exists(path)) {
        ret = ERROR_DVR_CANNOT_APPEND;
        srs_error("DVR can't append to exists path=%s. ret=%d", path.c_str(), ret);
        return ret;
    }
    fragment->set_path(path);
    
    // create dir first.
    if ((ret = fragment->create_dir()) != ERROR_SUCCESS) {
        return ret;
    }
    
    // create jitter.
    srs_freep(jitter);
    jitter = new SrsRtmpJitter();
    
    // open file writer, in append or create mode.
    string tmp_dvr_file = fragment->tmppath();
    if ((ret = fs->open(tmp_dvr_file)) != ERROR_SUCCESS) {
        srs_error("open file stream for file %s failed. ret=%d", path.c_str(), ret);
        return ret;
    }
    
    // initialize the encoder.
    if ((ret = open_encoder()) != ERROR_SUCCESS) {
        srs_error("initialize enc by fs for file %s failed. ret=%d", path.c_str(), ret);
        return ret;
    }
    
    srs_trace("dvr stream %s to file %s", req->stream.c_str(), path.c_str());
    
    return ret;
}
该函数调用open_encoder实际调用的是SrsDvrFlvSegementer::open_encoder

int SrsDvrFlvSegmenter::open_encoder()
{
    int ret = ERROR_SUCCESS;
    
    has_keyframe = false;
    
    // update the duration and filesize offset.
    duration_offset = 0;
    filesize_offset = 0;
    
    srs_freep(enc);
    enc = new SrsFlvTransmuxer();
    
    if ((ret = enc->initialize(fs)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // write the flv header to writer.
    if ((ret = enc->write_header()) != ERROR_SUCCESS) {
        srs_error("write flv header failed. ret=%d", ret);
        return ret;
    }
    
    return ret;
}
该函数首先创建flv 编码器对象SrsFlvTransmuxer,通过调用enc->initialize方法将SrsFileWritter(打开的flv文件生成的)与编码器关联,最后调用enc->write_header将flv文件头部写入。


3.SrsOriginHub::on_meta_data接收到metadata时会调用SrsDvrPlan::on_meta_data

int SrsDvrPlan::on_meta_data(SrsSharedPtrMessage* shared_metadata)
{
    int ret = ERROR_SUCCESS;
    
    if (!dvr_enabled) {
        return ret;
    }
    
    return segment->write_metadata(shared_metadata);
}

int SrsDvrSegmenter::write_metadata(SrsSharedPtrMessage* metadata)
{
    return encode_metadata(metadata);
}
该函数调用encode_metadata实际调用的是SrsDvrFlvSegementer::encode_metadata
int SrsDvrFlvSegmenter::encode_metadata(SrsSharedPtrMessage* metadata)
{
    int ret = ERROR_SUCCESS;
    
    // Ignore when metadata already written.
    if (duration_offset || filesize_offset) {
        return ret;
    }
    
    SrsBuffer stream;
    if ((ret = stream.initialize(metadata->payload, metadata->size)) != ERROR_SUCCESS) {
        return ret;
    }
    
    SrsAmf0Any* name = SrsAmf0Any::str();
    SrsAutoFree(SrsAmf0Any, name);
    if ((ret = name->read(&stream)) != ERROR_SUCCESS) {
        return ret;
    }
    
    SrsAmf0Object* obj = SrsAmf0Any::object();
    SrsAutoFree(SrsAmf0Object, obj);
    if ((ret = obj->read(&stream)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // remove duration and filesize.
    obj->set("filesize", NULL);
    obj->set("duration", NULL);
    
    // add properties.
    obj->set("service", SrsAmf0Any::str(RTMP_SIG_SRS_SERVER));
    obj->set("filesize", SrsAmf0Any::number(0));
    obj->set("duration", SrsAmf0Any::number(0));
    
    int size = name->total_size() + obj->total_size();
    char* payload = new char[size];
    SrsAutoFreeA(char, payload);
    
    // 11B flv header, 3B object EOF, 8B number value, 1B number flag.
    duration_offset = fs->tellg() + size + 11 - SrsAmf0Size::object_eof() - SrsAmf0Size::number();
    // 2B string flag, 8B number value, 8B string 'duration', 1B number flag
    filesize_offset = duration_offset - SrsAmf0Size::utf8("duration") - SrsAmf0Size::number();
    
    // convert metadata to bytes.
    if ((ret = stream.initialize(payload, size)) != ERROR_SUCCESS) {
        return ret;
    }
    if ((ret = name->write(&stream)) != ERROR_SUCCESS) {
        return ret;
    }
    if ((ret = obj->write(&stream)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // to flv file.
    if ((ret = enc->write_metadata(18, payload, size)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}
该函数首先解析接收到的metadata,然后将filesize和duration置空,增加一些属性,接着调用enc->write_metadata将数据写入到流中。

该函数还会记录下duration_offset, filesize_offset的位置,在录像结束获取到视频的大小和时长时能够再次进行更新。

4.SrsOriginHub::on_video接收到视频数据时会调用SrsDvrPlan::on_video

int SrsDvrPlan::on_video(SrsSharedPtrMessage* shared_video, SrsFormat* format)
{
    int ret = ERROR_SUCCESS;
    
    if (!dvr_enabled) {
        return ret;
    }
    
    if ((ret = segment->write_video(shared_video, format)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}

int SrsDvrSegmenter::write_video(SrsSharedPtrMessage* shared_video, SrsFormat* format)
{
    int ret = ERROR_SUCCESS;
    
    SrsSharedPtrMessage* video = shared_video->copy();
    SrsAutoFree(SrsSharedPtrMessage, video);
    
    if ((jitter->correct(video, jitter_algorithm)) != ERROR_SUCCESS) {
        return ret;
    }
    
    if ((ret = encode_video(video, format)) != ERROR_SUCCESS) {
        return ret;
    }
    
    if ((ret = on_update_duration(video)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}
该函数调用encode_video实际调用的是SrsDvrFlvSegementer::encode_video
int SrsDvrFlvSegmenter::encode_video(SrsSharedPtrMessage* video, SrsFormat* format)
{
    int ret = ERROR_SUCCESS;
    
    char* payload = video->payload;
    int size = video->size;
    bool sh = (format->video->avc_packet_type == SrsVideoAvcFrameTraitSequenceHeader);
    bool keyframe = (!sh && format->video->frame_type == SrsVideoAvcFrameTypeKeyFrame);
    
    if (keyframe) {
        has_keyframe = true;
    }
    
    // accept the sequence header here.
    // when got no keyframe, ignore when should wait keyframe.
    if (!has_keyframe && !sh) {
        if (wait_keyframe) {
            srs_info("dvr: ignore when wait keyframe.");
            return ret;
        }
    }
    
    if ((ret = enc->write_video(video->timestamp, payload, size)) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}
该函数将调用enc->write_metadata将数据写入。

4.SrsOriginHub::on_audio接收到音频数据时会调用SrsDvrPlan::on_audio,on_audio的处理过程与on_video类似这里不做分析了。

5.SrsOriginHub::on_unpublish取消发布时会调用SrsDvrPlan::on_unpublish,实际调用的是SrsDvrSessionPlan::on_unpublish

void SrsDvrSessionPlan::on_unpublish()
{
    // support multiple publish.
    if (!dvr_enabled) {
        return;
    }
    
    // ignore error.
    int ret = segment->close();
    if (ret != ERROR_SUCCESS) {
        srs_warn("ignore flv close error. ret=%d", ret);
    }
    
    dvr_enabled = false;
}

int SrsDvrSegmenter::close()
{
    int ret = ERROR_SUCCESS;
    
    // ignore when already closed.
    if (!fs->is_open()) {
        return ret;
    }
    
    // Close the encoder, then close the fs object.
    if ((ret = close_encoder()) != ERROR_SUCCESS) {
        return ret;
    }
    
    fs->close();
    
    // when tmp flv file exists, reap it.
    if ((ret = fragment->rename()) != ERROR_SUCCESS) {
        return ret;
    }
    
    // TODO: FIXME: the http callback is async, which will trigger thread switch,
    //          so the on_video maybe invoked during the http callback, and error.
    if ((ret = plan->on_reap_segment()) != ERROR_SUCCESS) {
        srs_error("dvr: notify plan to reap segment failed. ret=%d", ret);
        return ret;
    }
    
    return ret;
}
该函数会调用close_encoder关闭编码器,调用fs->close关闭打开的文件,接着调用fragment->rename将tmp flv 文件重命名为不带tmp的文件。

close_encoder()会调用refresh_metadata()完成metadata中duration,filesize属性的更新。

int SrsDvrFlvSegmenter::refresh_metadata()
{
    int ret = ERROR_SUCCESS;
    
    // no duration or filesize specified.
    if (!duration_offset || !filesize_offset) {
        return ret;
    }
    
    int64_t cur = fs->tellg();
    
    // buffer to write the size.
    char* buf = new char[SrsAmf0Size::number()];
    SrsAutoFreeA(char, buf);
    
    SrsBuffer stream;
    if ((ret = stream.initialize(buf, SrsAmf0Size::number())) != ERROR_SUCCESS) {
        return ret;
    }
    
    // filesize to buf.
    SrsAmf0Any* size = SrsAmf0Any::number((double)cur);
    SrsAutoFree(SrsAmf0Any, size);
    
    stream.skip(-1 * stream.pos());
    if ((ret = size->write(&stream)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // update the flesize.
    fs->seek2(filesize_offset);
    if ((ret = fs->write(buf, SrsAmf0Size::number(), NULL)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // duration to buf
    SrsAmf0Any* dur = SrsAmf0Any::number((double)fragment->duration() / 1000.0);
    SrsAutoFree(SrsAmf0Any, dur);
    
    stream.skip(-1 * stream.pos());
    if ((ret = dur->write(&stream)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // update the duration
    fs->seek2(duration_offset);
    if ((ret = fs->write(buf, SrsAmf0Size::number(), NULL)) != ERROR_SUCCESS) {
        return ret;
    }
    
    // reset the offset.
    fs->seek2(cur);
    
    return ret;
}

生成完录像对象后,调用plan->on_reap_segment通过http回调将录像的文件路径通知出去。

int SrsDvrPlan::on_reap_segment()
{
    int ret = ERROR_SUCCESS;
    
    int cid = _srs_context->get_id();
    
    SrsFragment* fragment = segment->current();
    string fullpath = fragment->fullpath();
    
    if ((ret = async->execute(new SrsDvrAsyncCallOnDvr(cid, req, fullpath))) != ERROR_SUCCESS) {
        return ret;
    }
    
    return ret;
}
该函数实际将SrsDvrAsyncCallOnDvr做为task投递到SrsAsyncCallWorker::cycle()中去执行,最终调用SrsDvrAsyncCallOnDvr::call

int SrsDvrAsyncCallOnDvr::call()
{
    int ret = ERROR_SUCCESS;
    
    if (!_srs_config->get_vhost_http_hooks_enabled(req->vhost)) {
        return ret;
    }
    
    // the http hooks will cause context switch,
    // so we must copy all hooks for the on_connect may freed.
    // @see https://github.com/ossrs/srs/issues/475
    vector hooks;
    
    if (true) {
        SrsConfDirective* conf = _srs_config->get_vhost_on_dvr(req->vhost);
        
        if (!conf) {
            srs_info("ignore the empty http callback: on_dvr");
            return ret;
        }
        
        hooks = conf->args;
    }
    
    for (int i = 0; i < (int)hooks.size(); i++) {
        std::string url = hooks.at(i);
        if ((ret = SrsHttpHooks::on_dvr(cid, url, req, path)) != ERROR_SUCCESS) {
            srs_error("hook client on_dvr failed. url=%s, ret=%d", url.c_str(), ret);
            return ret;
        }
    }
    
    return ret;
}
该函数调用Http回调接口。

























你可能感兴趣的:(SRS,代码分析)