| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cast/standalone_sender/looping_file_sender.h" |
| |
| #include "util/trace_logging.h" |
| |
| namespace openscreen { |
| namespace cast { |
| |
| LoopingFileSender::LoopingFileSender(Environment* environment, |
| const char* path, |
| SenderSession::ConfiguredSenders senders, |
| int max_bitrate) |
| : env_(environment), |
| path_(path), |
| packet_router_(env_), |
| max_bitrate_(max_bitrate), |
| audio_encoder_(senders.audio_sender->config().channels, |
| StreamingOpusEncoder::kDefaultCastAudioFramesPerSecond, |
| senders.audio_sender), |
| video_encoder_(StreamingVp8Encoder::Parameters{}, |
| env_->task_runner(), |
| senders.video_sender), |
| next_task_(env_->now_function(), env_->task_runner()), |
| console_update_task_(env_->now_function(), env_->task_runner()) { |
| // Opus and Vp8 are the default values for the config, and if these are set |
| // to a different value that means we offered a codec that we do not |
| // support, which is a developer error. |
| OSP_CHECK(senders.audio_config.codec == AudioCodec::kOpus); |
| OSP_CHECK(senders.video_config.codec == VideoCodec::kVp8); |
| OSP_LOG_INFO << "Max allowed media bitrate (audio + video) will be " |
| << max_bitrate_; |
| bandwidth_being_utilized_ = max_bitrate_ / 2; |
| UpdateEncoderBitrates(); |
| |
| next_task_.Schedule([this] { SendFileAgain(); }, Alarm::kImmediately); |
| } |
| |
| LoopingFileSender::~LoopingFileSender() = default; |
| |
| void LoopingFileSender::UpdateEncoderBitrates() { |
| if (bandwidth_being_utilized_ >= kHighBandwidthThreshold) { |
| audio_encoder_.UseHighQuality(); |
| } else { |
| audio_encoder_.UseStandardQuality(); |
| } |
| video_encoder_.SetTargetBitrate(bandwidth_being_utilized_ - |
| audio_encoder_.GetBitrate()); |
| } |
| |
| void LoopingFileSender::ControlForNetworkCongestion() { |
| bandwidth_estimate_ = packet_router_.ComputeNetworkBandwidth(); |
| if (bandwidth_estimate_ > 0) { |
| // Don't ever try to use *all* of the network bandwidth! However, don't go |
| // below the absolute minimum requirement either. |
| constexpr double kGoodNetworkCitizenFactor = 0.8; |
| const int usable_bandwidth = std::max<int>( |
| kGoodNetworkCitizenFactor * bandwidth_estimate_, kMinRequiredBitrate); |
| |
| // See "congestion control" discussion in the class header comments for |
| // BandwidthEstimator. |
| if (usable_bandwidth > bandwidth_being_utilized_) { |
| constexpr double kConservativeIncrease = 1.1; |
| bandwidth_being_utilized_ = std::min<int>( |
| bandwidth_being_utilized_ * kConservativeIncrease, usable_bandwidth); |
| } else { |
| bandwidth_being_utilized_ = usable_bandwidth; |
| } |
| |
| // Repsect the user's maximum bitrate setting. |
| bandwidth_being_utilized_ = |
| std::min(bandwidth_being_utilized_, max_bitrate_); |
| |
| UpdateEncoderBitrates(); |
| } else { |
| // There is no current bandwidth estimate. So, nothing should be adjusted. |
| } |
| |
| next_task_.ScheduleFromNow([this] { ControlForNetworkCongestion(); }, |
| kCongestionCheckInterval); |
| } |
| |
| void LoopingFileSender::SendFileAgain() { |
| OSP_LOG_INFO << "Sending " << path_ << " (starts in one second)..."; |
| TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneSender); |
| |
| OSP_DCHECK_EQ(num_capturers_running_, 0); |
| num_capturers_running_ = 2; |
| capture_start_time_ = latest_frame_time_ = env_->now() + seconds(1); |
| audio_capturer_.emplace(env_, path_, audio_encoder_.num_channels(), |
| audio_encoder_.sample_rate(), capture_start_time_, |
| this); |
| video_capturer_.emplace(env_, path_, capture_start_time_, this); |
| |
| next_task_.ScheduleFromNow([this] { ControlForNetworkCongestion(); }, |
| kCongestionCheckInterval); |
| console_update_task_.Schedule([this] { UpdateStatusOnConsole(); }, |
| capture_start_time_); |
| } |
| |
| void LoopingFileSender::OnAudioData(const float* interleaved_samples, |
| int num_samples, |
| Clock::time_point capture_time) { |
| TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneSender); |
| latest_frame_time_ = std::max(capture_time, latest_frame_time_); |
| audio_encoder_.EncodeAndSend(interleaved_samples, num_samples, capture_time); |
| } |
| |
| void LoopingFileSender::OnVideoFrame(const AVFrame& av_frame, |
| Clock::time_point capture_time) { |
| TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneSender); |
| latest_frame_time_ = std::max(capture_time, latest_frame_time_); |
| StreamingVp8Encoder::VideoFrame frame{}; |
| frame.width = av_frame.width - av_frame.crop_left - av_frame.crop_right; |
| frame.height = av_frame.height - av_frame.crop_top - av_frame.crop_bottom; |
| frame.yuv_planes[0] = av_frame.data[0] + av_frame.crop_left + |
| av_frame.linesize[0] * av_frame.crop_top; |
| frame.yuv_planes[1] = av_frame.data[1] + av_frame.crop_left / 2 + |
| av_frame.linesize[1] * av_frame.crop_top / 2; |
| frame.yuv_planes[2] = av_frame.data[2] + av_frame.crop_left / 2 + |
| av_frame.linesize[2] * av_frame.crop_top / 2; |
| for (int i = 0; i < 3; ++i) { |
| frame.yuv_strides[i] = av_frame.linesize[i]; |
| } |
| // TODO(miu): Add performance metrics visual overlay (based on Stats |
| // callback). |
| video_encoder_.EncodeAndSend(frame, capture_time, {}); |
| } |
| |
| void LoopingFileSender::UpdateStatusOnConsole() { |
| const Clock::duration elapsed = latest_frame_time_ - capture_start_time_; |
| const auto seconds_part = to_seconds(elapsed); |
| const auto millis_part = to_milliseconds(elapsed - seconds_part); |
| // The control codes here attempt to erase the current line the cursor is |
| // on, and then print out the updated status text. If the terminal does not |
| // support simple ANSI escape codes, the following will still work, but |
| // there might sometimes be old status lines not getting erased (i.e., just |
| // partially overwritten). |
| fprintf(stdout, |
| "\r\x1b[2K\rLoopingFileSender: At %01" PRId64 |
| ".%03ds in file (est. network bandwidth: %d kbps). \n", |
| static_cast<int64_t>(seconds_part.count()), |
| static_cast<int>(millis_part.count()), bandwidth_estimate_ / 1024); |
| fflush(stdout); |
| |
| console_update_task_.ScheduleFromNow([this] { UpdateStatusOnConsole(); }, |
| kConsoleUpdateInterval); |
| } |
| |
| void LoopingFileSender::OnEndOfFile(SimulatedCapturer* capturer) { |
| OSP_LOG_INFO << "The " << ToTrackName(capturer) |
| << " capturer has reached the end of the media stream."; |
| --num_capturers_running_; |
| if (num_capturers_running_ == 0) { |
| console_update_task_.Cancel(); |
| next_task_.Schedule([this] { SendFileAgain(); }, Alarm::kImmediately); |
| } |
| } |
| |
| void LoopingFileSender::OnError(SimulatedCapturer* capturer, |
| std::string message) { |
| OSP_LOG_ERROR << "The " << ToTrackName(capturer) |
| << " has failed: " << message; |
| --num_capturers_running_; |
| // If both fail, the application just pauses. This accounts for things like |
| // "file not found" errors. However, if only one track fails, then keep |
| // going. |
| } |
| |
| const char* LoopingFileSender::ToTrackName(SimulatedCapturer* capturer) const { |
| const char* which; |
| if (capturer == &*audio_capturer_) { |
| which = "audio"; |
| } else if (capturer == &*video_capturer_) { |
| which = "video"; |
| } else { |
| OSP_NOTREACHED(); |
| which = ""; |
| } |
| return which; |
| } |
| |
| } // namespace cast |
| } // namespace openscreen |