Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for recording audio and screensharing streams #1265

Closed
wants to merge 13 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 41 additions & 8 deletions erizo/src/erizo/media/ExternalOutput.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@ ExternalOutput::ExternalOutput(std::shared_ptr<Worker> worker, const std::string
audio_stream_{nullptr}, video_source_ssrc_{0},
first_video_timestamp_{-1}, first_audio_timestamp_{-1},
first_data_received_{}, video_offset_ms_{-1}, audio_offset_ms_{-1},
need_to_send_fir_{true}, rtp_mappings_{rtp_mappings}, video_codec_{AV_CODEC_ID_NONE},
audio_codec_{AV_CODEC_ID_NONE}, pipeline_initialized_{false}, ext_processor_{ext_mappings} {
need_to_send_fir_{true}, rtp_mappings_{rtp_mappings}, hasAudio_{false}, hasVideo_{false},
video_codec_{AV_CODEC_ID_NONE}, audio_codec_{AV_CODEC_ID_NONE},
pipeline_initialized_{false}, ext_processor_{ext_mappings}
{
ELOG_DEBUG("Creating output to %s", output_url.c_str());

fb_sink_ = nullptr;
Expand All @@ -46,9 +48,11 @@ ExternalOutput::ExternalOutput(std::shared_ptr<Worker> worker, const std::string
switch (rtp_map.media_type) {
case AUDIO_TYPE:
audio_maps_[rtp_map.payload_type] = rtp_map;
hasAudio_ = true;
break;
case VIDEO_TYPE:
video_maps_[rtp_map.payload_type] = rtp_map;
hasVideo_ = true;
break;
case OTHER:
break;
Expand Down Expand Up @@ -89,6 +93,10 @@ bool ExternalOutput::init() {
return true;
}

void ExternalOutput::setHasAudioAndVideo(bool hasAudio, bool hasVideo) {
hasAudio_ = hasAudio;
hasVideo_ = hasVideo;
}

ExternalOutput::~ExternalOutput() {
ELOG_DEBUG("Destructing");
Expand Down Expand Up @@ -369,15 +377,27 @@ int ExternalOutput::deliverEvent_(MediaEventPtr event) {
}

bool ExternalOutput::initContext() {
if (video_codec_ != AV_CODEC_ID_NONE &&
audio_codec_ != AV_CODEC_ID_NONE &&
video_stream_ == nullptr &&
audio_stream_ == nullptr) {
bool init_video = false;
bool init_audio = false;

ELOG_DEBUG("hasVideo_: %d hasAudio_: %d video_codec_: %d audio_codec_: %d",
hasVideo_, hasAudio_, video_codec_, audio_codec_);

if (hasVideo_ && video_codec_ == AV_CODEC_ID_NONE) {
return false;
}

if (hasAudio_ && audio_codec_ == AV_CODEC_ID_NONE) {
return false;
}

if (hasVideo_ && video_stream_ == nullptr) {
AVCodec* video_codec = avcodec_find_encoder(video_codec_);
if (video_codec == nullptr) {
ELOG_ERROR("Could not find video codec");
return false;
}
init_video = true;
need_to_send_fir_ = true;
video_queue_.setTimebase(video_map_.clock_rate);
video_stream_ = avformat_new_stream(context_, video_codec);
Expand All @@ -394,13 +414,16 @@ bool ExternalOutput::initContext() {
video_stream_->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
context_->oformat->flags |= AVFMT_VARIABLE_FPS;
context_->streams[0] = video_stream_;
}

if (hasAudio_ && audio_stream_ == nullptr) {
AVCodec* audio_codec = avcodec_find_encoder(audio_codec_);
if (audio_codec == nullptr) {
ELOG_ERROR("Could not find audio codec");
return false;
}

init_audio = true;
audio_stream_ = avformat_new_stream(context_, audio_codec);
audio_stream_->id = 1;
audio_stream_->codec->codec_id = audio_codec_;
Expand All @@ -411,8 +434,18 @@ bool ExternalOutput::initContext() {
audio_stream_->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}

context_->streams[0] = video_stream_;
if (!hasVideo_) {
// To avoid the following matroska errors, we add CODEC_FLAG_GLOBAL_HEADER...
// - Codec for stream 0 does not use global headers but container format requires global headers
// - Only audio, video, and subtitles are supported for Matroska.
video_stream_ = avformat_new_stream(context_, nullptr);
video_stream_->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
context_->streams[0] = video_stream_;
}
context_->streams[1] = audio_stream_;
}

if ( init_audio || init_video ) {
if (avio_open(&context_->pb, context_->filename, AVIO_FLAG_WRITE) < 0) {
ELOG_ERROR("Error opening output file");
return false;
Expand Down
5 changes: 4 additions & 1 deletion erizo/src/erizo/media/ExternalOutput.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ class ExternalOutput : public MediaSink, public RawDataReceiver, public Feedback

bool isRecording() { return recording_; }

void setHasAudioAndVideo(bool hasAudio, bool hasVideo);

private:
std::shared_ptr<Worker> worker_;
Pipeline::Ptr pipeline_;
Expand All @@ -68,7 +70,6 @@ class ExternalOutput : public MediaSink, public RawDataReceiver, public Feedback
boost::condition_variable cond_;
AVStream *video_stream_, *audio_stream_;
AVFormatContext *context_;

uint32_t video_source_ssrc_;
std::unique_ptr<Depacketizer> depacketizer_;

Expand Down Expand Up @@ -107,6 +108,8 @@ class ExternalOutput : public MediaSink, public RawDataReceiver, public Feedback
// so the second scheme seems not applicable. Too bad.
bool need_to_send_fir_;
std::vector<RtpMap> rtp_mappings_;
bool hasAudio_;
bool hasVideo_;
enum AVCodecID video_codec_;
enum AVCodecID audio_codec_;
std::map<uint, RtpMap> video_maps_;
Expand Down
13 changes: 13 additions & 0 deletions erizoAPI/ExternalOutput.cc
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ NAN_MODULE_INIT(ExternalOutput::Init) {
// Prototype
Nan::SetPrototypeMethod(tpl, "close", close);
Nan::SetPrototypeMethod(tpl, "init", init);
Nan::SetPrototypeMethod(tpl, "setHasAudioAndVideo", setHasAudioAndVideo);

constructor.Reset(tpl->GetFunction());
Nan::Set(target, Nan::New("ExternalOutput").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked());
Expand Down Expand Up @@ -157,3 +158,15 @@ NAN_METHOD(ExternalOutput::init) {
int r = me->init();
info.GetReturnValue().Set(Nan::New(r));
}

NAN_METHOD(ExternalOutput::setHasAudioAndVideo) {
ExternalOutput* obj = ObjectWrap::Unwrap<ExternalOutput>(info.Holder());
std::shared_ptr<erizo::ExternalOutput> me = obj->me;

bool hasAudio = (bool) info[0]->BooleanValue();
bool hasVideo = (bool) info[1]->BooleanValue();

me->setHasAudioAndVideo(hasAudio, hasVideo);
}


2 changes: 2 additions & 0 deletions erizoAPI/ExternalOutput.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class ExternalOutput: public MediaSink {
*/
static NAN_METHOD(init);

static NAN_METHOD(setHasAudioAndVideo);

static Nan::Persistent<v8::Function> constructor;
};

Expand Down
13 changes: 9 additions & 4 deletions erizo_controller/erizoController/models/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,10 +371,15 @@ class Client extends events.EventEmitter {
const stream = this.room.getStreamById(streamId);

if (stream.hasAudio() || stream.hasVideo() || stream.hasScreen()) {
const mediaOptions = { mediaConfiguration: this.token.mediaConfiguration };
this.room.controller.addExternalOutput(streamId, url, mediaOptions, (result) => {
if (result === 'success') {
log.info('message: startRecorder, ' +
const mediaOptions = {
mediaConfiguration: this.token.mediaConfiguration,
hasAudio: stream.hasAudio(),
hasVideo: stream.hasVideo() || stream.hasScreen()
};

this.room.controller.addExternalOutput(streamId, url, mediaOptions, function (result) {
if (result === 'success') {
log.info('message: startRecorder, ' +
'state: RECORD_STARTED, ' +
`streamId: ${streamId}, ` +
`url: ${url}`);
Expand Down
4 changes: 4 additions & 0 deletions erizo_controller/erizoJS/models/Publisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,14 @@ class Source extends NodeClass {

addExternalOutput(url, options) {
const eoId = `${url}_${this.streamId}`;
const hasVideo = options.hasVideo === null || options.hasVideo;
const hasAudio = options.hasAudio === null || options.hasAudio;
log.info(`message: Adding ExternalOutput, id: ${eoId}, url: ${url}`);
const externalOutput = new addon.ExternalOutput(this.threadPool, url,
Helpers.getMediaConfiguration(options.mediaConfiguration));
externalOutput.id = eoId;
externalOutput.setHasAudioAndVideo(hasAudio, hasVideo);

externalOutput.init();
this.muxer.addExternalOutput(externalOutput, url);
this.externalOutputs[url] = externalOutput;
Expand Down
1 change: 1 addition & 0 deletions erizo_controller/test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ module.exports.reset = () => {
module.exports.ExternalOutput = {
init: sinon.stub(),
close: sinon.stub(),
setHasAudioAndVideo: sinon.stub()
};

module.exports.erizoAPI = createMock('../../erizoAPI/build/Release/addon', {
Expand Down