| // Copyright 2014 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. |
| |
| // Simulate end to end streaming. |
| // |
| // Input: |
| // --source= |
| // WebM used as the source of video and audio frames. |
| // --output= |
| // File path to writing out the raw event log of the simulation session. |
| // --sim-id= |
| // Unique simulation ID. |
| // --target-delay-ms= |
| // Target playout delay to configure (integer number of milliseconds). |
| // Optional; default is 400. |
| // --max-frame-rate= |
| // The maximum frame rate allowed at any time during the Cast session. |
| // Optional; default is 30. |
| // --source-frame-rate= |
| // Overrides the playback rate; the source video will play faster/slower. |
| // --run-time= |
| // In seconds, how long the Cast session runs for. |
| // Optional; default is 180. |
| // --metrics-output= |
| // File path to write PSNR and SSIM metrics between source frames and |
| // decoded frames. Assumes all encoded frames are decoded. |
| // --yuv-output= |
| // File path to write YUV decoded frames in YUV4MPEG2 format. |
| // --no-simulation |
| // Do not run network simulation. |
| // |
| // Output: |
| // - Raw event log of the simulation session tagged with the unique test ID, |
| // written out to the specified file path. |
| |
| #include <utility> |
| |
| #include "base/at_exit.h" |
| #include "base/base_paths.h" |
| #include "base/command_line.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/memory_mapped_file.h" |
| #include "base/files/scoped_file.h" |
| #include "base/json/json_writer.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/path_service.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/tick_clock.h" |
| #include "base/values.h" |
| #include "media/base/audio_bus.h" |
| #include "media/base/fake_single_thread_task_runner.h" |
| #include "media/base/media.h" |
| #include "media/base/video_frame.h" |
| #include "media/cast/cast_config.h" |
| #include "media/cast/cast_environment.h" |
| #include "media/cast/cast_receiver.h" |
| #include "media/cast/cast_sender.h" |
| #include "media/cast/logging/encoding_event_subscriber.h" |
| #include "media/cast/logging/log_serializer.h" |
| #include "media/cast/logging/logging_defines.h" |
| #include "media/cast/logging/proto/raw_events.pb.h" |
| #include "media/cast/logging/raw_event_subscriber_bundle.h" |
| #include "media/cast/logging/simple_event_subscriber.h" |
| #include "media/cast/net/cast_transport.h" |
| #include "media/cast/net/cast_transport_config.h" |
| #include "media/cast/net/cast_transport_defines.h" |
| #include "media/cast/net/cast_transport_impl.h" |
| #include "media/cast/test/fake_media_source.h" |
| #include "media/cast/test/loopback_transport.h" |
| #include "media/cast/test/proto/network_simulation_model.pb.h" |
| #include "media/cast/test/skewed_tick_clock.h" |
| #include "media/cast/test/utility/audio_utility.h" |
| #include "media/cast/test/utility/default_config.h" |
| #include "media/cast/test/utility/test_util.h" |
| #include "media/cast/test/utility/udp_proxy.h" |
| #include "media/cast/test/utility/video_utility.h" |
| #include "starboard/types.h" |
| |
| using media::cast::proto::IPPModel; |
| using media::cast::proto::NetworkSimulationModel; |
| using media::cast::proto::NetworkSimulationModelType; |
| |
| namespace cobalt { |
| namespace media { |
| namespace cast { |
| namespace { |
| const char kLibDir[] = "lib-dir"; |
| const char kModelPath[] = "model"; |
| const char kMetricsOutputPath[] = "metrics-output"; |
| const char kOutputPath[] = "output"; |
| const char kMaxFrameRate[] = "max-frame-rate"; |
| const char kNoSimulation[] = "no-simulation"; |
| const char kRunTime[] = "run-time"; |
| const char kSimulationId[] = "sim-id"; |
| const char kSourcePath[] = "source"; |
| const char kSourceFrameRate[] = "source-frame-rate"; |
| const char kTargetDelay[] = "target-delay-ms"; |
| const char kYuvOutputPath[] = "yuv-output"; |
| |
| int GetIntegerSwitchValue(const char* switch_name, int default_value) { |
| const std::string as_str = |
| base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(switch_name); |
| if (as_str.empty()) return default_value; |
| int as_int; |
| CHECK(base::StringToInt(as_str, &as_int)); |
| CHECK_GT(as_int, 0); |
| return as_int; |
| } |
| |
| void LogAudioOperationalStatus(OperationalStatus status) { |
| LOG(INFO) << "Audio status: " << status; |
| } |
| |
| void LogVideoOperationalStatus(OperationalStatus status) { |
| LOG(INFO) << "Video status: " << status; |
| } |
| |
| struct PacketProxy { |
| PacketProxy() : receiver(NULL) {} |
| void ReceivePacket(std::unique_ptr<Packet> packet) { |
| if (receiver) receiver->ReceivePacket(std::move(packet)); |
| } |
| CastReceiver* receiver; |
| }; |
| |
| class TransportClient : public CastTransport::Client { |
| public: |
| TransportClient(LogEventDispatcher* log_event_dispatcher, |
| PacketProxy* packet_proxy) |
| : log_event_dispatcher_(log_event_dispatcher), |
| packet_proxy_(packet_proxy) {} |
| |
| void OnStatusChanged(CastTransportStatus status) final { |
| LOG(INFO) << "Cast transport status: " << status; |
| }; |
| void OnLoggingEventsReceived( |
| std::unique_ptr<std::vector<FrameEvent>> frame_events, |
| std::unique_ptr<std::vector<PacketEvent>> packet_events) final { |
| DCHECK(log_event_dispatcher_); |
| log_event_dispatcher_->DispatchBatchOfEvents(std::move(frame_events), |
| std::move(packet_events)); |
| }; |
| void ProcessRtpPacket(std::unique_ptr<Packet> packet) final { |
| if (packet_proxy_) packet_proxy_->ReceivePacket(std::move(packet)); |
| }; |
| |
| private: |
| LogEventDispatcher* const log_event_dispatcher_; // Not owned by this class. |
| PacketProxy* const packet_proxy_; // Not owned by this class. |
| |
| DISALLOW_COPY_AND_ASSIGN(TransportClient); |
| }; |
| |
| // Maintains a queue of encoded video frames. |
| // This works by tracking FRAME_CAPTURE_END and FRAME_ENCODED events. |
| // If a video frame is detected to be encoded it transfers a frame |
| // from FakeMediaSource to its internal queue. Otherwise it drops a |
| // frame from FakeMediaSource. |
| class EncodedVideoFrameTracker : public RawEventSubscriber { |
| public: |
| explicit EncodedVideoFrameTracker(FakeMediaSource* media_source) |
| : media_source_(media_source), last_frame_event_type_(UNKNOWN) {} |
| ~EncodedVideoFrameTracker() final {} |
| |
| // RawEventSubscriber implementations. |
| void OnReceiveFrameEvent(const FrameEvent& frame_event) final { |
| // This method only cares about video FRAME_CAPTURE_END and |
| // FRAME_ENCODED events. |
| if (frame_event.media_type != VIDEO_EVENT) { |
| return; |
| } |
| if (frame_event.type != FRAME_CAPTURE_END && |
| frame_event.type != FRAME_ENCODED) { |
| return; |
| } |
| // If there are two consecutive FRAME_CAPTURE_END events that means |
| // a frame is dropped. |
| if (last_frame_event_type_ == FRAME_CAPTURE_END && |
| frame_event.type == FRAME_CAPTURE_END) { |
| media_source_->PopOldestInsertedVideoFrame(); |
| } |
| if (frame_event.type == FRAME_ENCODED) { |
| video_frames_.push(media_source_->PopOldestInsertedVideoFrame()); |
| } |
| last_frame_event_type_ = frame_event.type; |
| } |
| |
| void OnReceivePacketEvent(const PacketEvent& packet_event) final { |
| // Don't care. |
| } |
| |
| scoped_refptr<media::VideoFrame> PopOldestEncodedFrame() { |
| CHECK(!video_frames_.empty()); |
| scoped_refptr<media::VideoFrame> video_frame = video_frames_.front(); |
| video_frames_.pop(); |
| return video_frame; |
| } |
| |
| private: |
| FakeMediaSource* media_source_; |
| CastLoggingEvent last_frame_event_type_; |
| std::queue<scoped_refptr<media::VideoFrame>> video_frames_; |
| |
| DISALLOW_COPY_AND_ASSIGN(EncodedVideoFrameTracker); |
| }; |
| |
| // Appends a YUV frame in I420 format to the file located at |path|. |
| void AppendYuvToFile(const base::FilePath& path, |
| scoped_refptr<media::VideoFrame> frame) { |
| // Write YUV420 format to file. |
| std::string header; |
| base::StringAppendF(&header, "FRAME W%d H%d\n", frame->coded_size().width(), |
| frame->coded_size().height()); |
| AppendToFile(path, header.data(), header.size()); |
| AppendToFile(path, |
| reinterpret_cast<char*>(frame->data(media::VideoFrame::kYPlane)), |
| frame->stride(media::VideoFrame::kYPlane) * |
| frame->rows(media::VideoFrame::kYPlane)); |
| AppendToFile(path, |
| reinterpret_cast<char*>(frame->data(media::VideoFrame::kUPlane)), |
| frame->stride(media::VideoFrame::kUPlane) * |
| frame->rows(media::VideoFrame::kUPlane)); |
| AppendToFile(path, |
| reinterpret_cast<char*>(frame->data(media::VideoFrame::kVPlane)), |
| frame->stride(media::VideoFrame::kVPlane) * |
| frame->rows(media::VideoFrame::kVPlane)); |
| } |
| |
| // A container to save output of GotVideoFrame() for computation based |
| // on output frames. |
| struct GotVideoFrameOutput { |
| GotVideoFrameOutput() : counter(0) {} |
| int counter; |
| std::vector<double> psnr; |
| std::vector<double> ssim; |
| }; |
| |
| void GotVideoFrame(GotVideoFrameOutput* metrics_output, |
| const base::FilePath& yuv_output, |
| EncodedVideoFrameTracker* video_frame_tracker, |
| CastReceiver* cast_receiver, |
| const scoped_refptr<media::VideoFrame>& video_frame, |
| const base::TimeTicks& render_time, bool continuous) { |
| ++metrics_output->counter; |
| cast_receiver->RequestDecodedVideoFrame( |
| base::Bind(&GotVideoFrame, metrics_output, yuv_output, |
| video_frame_tracker, cast_receiver)); |
| |
| // If |video_frame_tracker| is available that means we're computing |
| // quality metrices. |
| if (video_frame_tracker) { |
| scoped_refptr<media::VideoFrame> src_frame = |
| video_frame_tracker->PopOldestEncodedFrame(); |
| metrics_output->psnr.push_back(I420PSNR(src_frame, video_frame)); |
| metrics_output->ssim.push_back(I420SSIM(src_frame, video_frame)); |
| } |
| |
| if (!yuv_output.empty()) { |
| AppendYuvToFile(yuv_output, video_frame); |
| } |
| } |
| |
| void GotAudioFrame(int* counter, CastReceiver* cast_receiver, |
| std::unique_ptr<AudioBus> audio_bus, |
| const base::TimeTicks& playout_time, bool is_continuous) { |
| ++*counter; |
| cast_receiver->RequestDecodedAudioFrame( |
| base::Bind(&GotAudioFrame, counter, cast_receiver)); |
| } |
| |
| // Serialize |frame_events| and |packet_events| and append to the file |
| // located at |output_path|. |
| void AppendLogToFile(media::cast::proto::LogMetadata* metadata, |
| const media::cast::FrameEventList& frame_events, |
| const media::cast::PacketEventList& packet_events, |
| const base::FilePath& output_path) { |
| media::cast::proto::GeneralDescription* gen_desc = |
| metadata->mutable_general_description(); |
| gen_desc->set_product("Cast Simulator"); |
| gen_desc->set_product_version("0.1"); |
| |
| std::unique_ptr<char[]> serialized_log( |
| new char[media::cast::kMaxSerializedBytes]); |
| int output_bytes; |
| bool success = media::cast::SerializeEvents( |
| *metadata, frame_events, packet_events, true, |
| media::cast::kMaxSerializedBytes, serialized_log.get(), &output_bytes); |
| |
| if (!success) { |
| LOG(ERROR) << "Failed to serialize log."; |
| return; |
| } |
| |
| if (!AppendToFile(output_path, serialized_log.get(), output_bytes)) { |
| LOG(ERROR) << "Failed to append to log."; |
| } |
| } |
| |
| // Run simulation once. |
| // |
| // |log_output_path| is the path to write serialized log. |
| // |extra_data| is extra tagging information to write to log. |
| void RunSimulation(const base::FilePath& source_path, |
| const base::FilePath& log_output_path, |
| const base::FilePath& metrics_output_path, |
| const base::FilePath& yuv_output_path, |
| const std::string& extra_data, |
| const NetworkSimulationModel& model) { |
| // Fake clock. Make sure start time is non zero. |
| base::SimpleTestTickClock testing_clock; |
| testing_clock.Advance(base::TimeDelta::FromSeconds(1)); |
| |
| // Task runner. |
| scoped_refptr<FakeSingleThreadTaskRunner> task_runner = |
| new FakeSingleThreadTaskRunner(&testing_clock); |
| base::ThreadTaskRunnerHandle task_runner_handle(task_runner); |
| |
| // CastEnvironments. |
| scoped_refptr<CastEnvironment> sender_env = |
| new CastEnvironment(std::unique_ptr<base::TickClock>( |
| new test::SkewedTickClock(&testing_clock)), |
| task_runner, task_runner, task_runner); |
| scoped_refptr<CastEnvironment> receiver_env = |
| new CastEnvironment(std::unique_ptr<base::TickClock>( |
| new test::SkewedTickClock(&testing_clock)), |
| task_runner, task_runner, task_runner); |
| |
| // Event subscriber. Store at most 1 hour of events. |
| EncodingEventSubscriber audio_event_subscriber(AUDIO_EVENT, 100 * 60 * 60); |
| EncodingEventSubscriber video_event_subscriber(VIDEO_EVENT, 30 * 60 * 60); |
| sender_env->logger()->Subscribe(&audio_event_subscriber); |
| sender_env->logger()->Subscribe(&video_event_subscriber); |
| |
| // Audio sender config. |
| FrameSenderConfig audio_sender_config = GetDefaultAudioSenderConfig(); |
| audio_sender_config.min_playout_delay = |
| audio_sender_config.max_playout_delay = base::TimeDelta::FromMilliseconds( |
| GetIntegerSwitchValue(kTargetDelay, 400)); |
| |
| // Audio receiver config. |
| FrameReceiverConfig audio_receiver_config = GetDefaultAudioReceiverConfig(); |
| audio_receiver_config.rtp_max_delay_ms = |
| audio_sender_config.max_playout_delay.InMilliseconds(); |
| |
| // Video sender config. |
| FrameSenderConfig video_sender_config = GetDefaultVideoSenderConfig(); |
| video_sender_config.max_bitrate = 2500000; |
| video_sender_config.min_bitrate = 2000000; |
| video_sender_config.start_bitrate = 2000000; |
| video_sender_config.min_playout_delay = |
| video_sender_config.max_playout_delay = |
| audio_sender_config.max_playout_delay; |
| video_sender_config.max_frame_rate = GetIntegerSwitchValue(kMaxFrameRate, 30); |
| |
| // Video receiver config. |
| FrameReceiverConfig video_receiver_config = GetDefaultVideoReceiverConfig(); |
| video_receiver_config.rtp_max_delay_ms = |
| video_sender_config.max_playout_delay.InMilliseconds(); |
| |
| // Loopback transport. Owned by CastTransport. |
| LoopBackTransport* receiver_to_sender = new LoopBackTransport(receiver_env); |
| LoopBackTransport* sender_to_receiver = new LoopBackTransport(sender_env); |
| |
| PacketProxy packet_proxy; |
| |
| // Cast receiver. |
| std::unique_ptr<CastTransport> transport_receiver(new CastTransportImpl( |
| &testing_clock, base::TimeDelta::FromSeconds(1), |
| base::MakeUnique<TransportClient>(receiver_env->logger(), &packet_proxy), |
| base::WrapUnique(receiver_to_sender), task_runner)); |
| std::unique_ptr<CastReceiver> cast_receiver( |
| CastReceiver::Create(receiver_env, audio_receiver_config, |
| video_receiver_config, transport_receiver.get())); |
| |
| packet_proxy.receiver = cast_receiver.get(); |
| |
| // Cast sender and transport sender. |
| std::unique_ptr<CastTransport> transport_sender(new CastTransportImpl( |
| &testing_clock, base::TimeDelta::FromSeconds(1), |
| base::MakeUnique<TransportClient>(sender_env->logger(), nullptr), |
| base::WrapUnique(sender_to_receiver), task_runner)); |
| std::unique_ptr<CastSender> cast_sender( |
| CastSender::Create(sender_env, transport_sender.get())); |
| |
| // Initialize network simulation model. |
| const bool use_network_simulation = |
| model.type() == media::cast::proto::INTERRUPTED_POISSON_PROCESS; |
| std::unique_ptr<test::InterruptedPoissonProcess> ipp; |
| if (use_network_simulation) { |
| LOG(INFO) << "Running Poisson based network simulation."; |
| const IPPModel& ipp_model = model.ipp(); |
| std::vector<double> average_rates(ipp_model.average_rate_size()); |
| std::copy(ipp_model.average_rate().begin(), ipp_model.average_rate().end(), |
| average_rates.begin()); |
| ipp.reset(new test::InterruptedPoissonProcess( |
| average_rates, ipp_model.coef_burstiness(), ipp_model.coef_variance(), |
| 0)); |
| receiver_to_sender->Initialize(ipp->NewBuffer(128 * 1024), |
| transport_sender->PacketReceiverForTesting(), |
| task_runner, &testing_clock); |
| sender_to_receiver->Initialize( |
| ipp->NewBuffer(128 * 1024), |
| transport_receiver->PacketReceiverForTesting(), task_runner, |
| &testing_clock); |
| } else { |
| LOG(INFO) << "No network simulation."; |
| receiver_to_sender->Initialize(std::unique_ptr<test::PacketPipe>(), |
| transport_sender->PacketReceiverForTesting(), |
| task_runner, &testing_clock); |
| sender_to_receiver->Initialize( |
| std::unique_ptr<test::PacketPipe>(), |
| transport_receiver->PacketReceiverForTesting(), task_runner, |
| &testing_clock); |
| } |
| |
| // Initialize a fake media source and a tracker to encoded video frames. |
| const bool quality_test = !metrics_output_path.empty(); |
| FakeMediaSource media_source(task_runner, &testing_clock, audio_sender_config, |
| video_sender_config, quality_test); |
| std::unique_ptr<EncodedVideoFrameTracker> video_frame_tracker; |
| if (quality_test) { |
| video_frame_tracker.reset(new EncodedVideoFrameTracker(&media_source)); |
| sender_env->logger()->Subscribe(video_frame_tracker.get()); |
| } |
| |
| // Quality metrics computed for each frame decoded. |
| GotVideoFrameOutput metrics_output; |
| |
| // Start receiver. |
| int audio_frame_count = 0; |
| cast_receiver->RequestDecodedVideoFrame( |
| base::Bind(&GotVideoFrame, &metrics_output, yuv_output_path, |
| video_frame_tracker.get(), cast_receiver.get())); |
| cast_receiver->RequestDecodedAudioFrame( |
| base::Bind(&GotAudioFrame, &audio_frame_count, cast_receiver.get())); |
| |
| // Initializing audio and video senders. |
| cast_sender->InitializeAudio(audio_sender_config, |
| base::Bind(&LogAudioOperationalStatus)); |
| cast_sender->InitializeVideo(media_source.get_video_config(), |
| base::Bind(&LogVideoOperationalStatus), |
| CreateDefaultVideoEncodeAcceleratorCallback(), |
| CreateDefaultVideoEncodeMemoryCallback()); |
| task_runner->RunTasks(); |
| |
| // Truncate YUV files to prepare for writing. |
| if (!yuv_output_path.empty()) { |
| base::ScopedFILE file(base::OpenFile(yuv_output_path, "wb")); |
| if (!file.get()) { |
| LOG(ERROR) << "Cannot save YUV output to file."; |
| return; |
| } |
| LOG(INFO) << "Writing YUV output to file: " << yuv_output_path.value(); |
| |
| // Write YUV4MPEG2 header. |
| const std::string header("YUV4MPEG2 W1280 H720 F30000:1001 Ip A1:1 C420\n"); |
| AppendToFile(yuv_output_path, header.data(), header.size()); |
| } |
| |
| // Start sending. |
| if (!source_path.empty()) { |
| // 0 means using the FPS from the file. |
| media_source.SetSourceFile(source_path, |
| GetIntegerSwitchValue(kSourceFrameRate, 0)); |
| } |
| media_source.Start(cast_sender->audio_frame_input(), |
| cast_sender->video_frame_input()); |
| |
| // By default runs simulation for 3 minutes or the desired duration |
| // by using --run-time= flag. |
| base::TimeDelta elapsed_time; |
| const base::TimeDelta desired_run_time = |
| base::TimeDelta::FromSeconds(GetIntegerSwitchValue(kRunTime, 180)); |
| while (elapsed_time < desired_run_time) { |
| // Each step is 100us. |
| base::TimeDelta step = base::TimeDelta::FromMicroseconds(100); |
| task_runner->Sleep(step); |
| elapsed_time += step; |
| } |
| |
| // Unsubscribe from logging events. |
| sender_env->logger()->Unsubscribe(&audio_event_subscriber); |
| sender_env->logger()->Unsubscribe(&video_event_subscriber); |
| if (quality_test) |
| sender_env->logger()->Unsubscribe(video_frame_tracker.get()); |
| |
| // Get event logs for audio and video. |
| media::cast::proto::LogMetadata audio_metadata, video_metadata; |
| media::cast::FrameEventList audio_frame_events, video_frame_events; |
| media::cast::PacketEventList audio_packet_events, video_packet_events; |
| audio_metadata.set_extra_data(extra_data); |
| video_metadata.set_extra_data(extra_data); |
| audio_event_subscriber.GetEventsAndReset(&audio_metadata, &audio_frame_events, |
| &audio_packet_events); |
| video_event_subscriber.GetEventsAndReset(&video_metadata, &video_frame_events, |
| &video_packet_events); |
| |
| // Print simulation results. |
| |
| // Compute and print statistics for video: |
| // |
| // * Total video frames captured. |
| // * Total video frames encoded. |
| // * Total video frames dropped. |
| // * Total video frames received late. |
| // * Average target bitrate. |
| // * Average encoded bitrate. |
| int total_video_frames = 0; |
| int encoded_video_frames = 0; |
| int dropped_video_frames = 0; |
| int late_video_frames = 0; |
| int64_t total_delay_of_late_frames_ms = 0; |
| int64_t encoded_size = 0; |
| int64_t target_bitrate = 0; |
| for (size_t i = 0; i < video_frame_events.size(); ++i) { |
| const media::cast::proto::AggregatedFrameEvent& event = |
| *video_frame_events[i]; |
| ++total_video_frames; |
| if (event.has_encoded_frame_size()) { |
| ++encoded_video_frames; |
| encoded_size += event.encoded_frame_size(); |
| target_bitrate += event.target_bitrate(); |
| } else { |
| ++dropped_video_frames; |
| } |
| if (event.has_delay_millis() && event.delay_millis() < 0) { |
| ++late_video_frames; |
| total_delay_of_late_frames_ms += -event.delay_millis(); |
| } |
| } |
| |
| // Subtract fraction of dropped frames from |elapsed_time| before estimating |
| // the average encoded bitrate. |
| const base::TimeDelta elapsed_time_undropped = |
| total_video_frames <= 0 |
| ? base::TimeDelta() |
| : (elapsed_time * (total_video_frames - dropped_video_frames) / |
| total_video_frames); |
| const double avg_encoded_bitrate = |
| elapsed_time_undropped <= base::TimeDelta() |
| ? 0 |
| : 8.0 * encoded_size / elapsed_time_undropped.InSecondsF() / 1000; |
| double avg_target_bitrate = |
| !encoded_video_frames ? 0 : target_bitrate / encoded_video_frames / 1000; |
| |
| LOG(INFO) << "Configured target playout delay (ms): " |
| << video_receiver_config.rtp_max_delay_ms; |
| LOG(INFO) << "Audio frame count: " << audio_frame_count; |
| LOG(INFO) << "Inserted video frames: " << total_video_frames; |
| LOG(INFO) << "Decoded video frames: " << metrics_output.counter; |
| LOG(INFO) << "Dropped video frames: " << dropped_video_frames; |
| LOG(INFO) << "Late video frames: " << late_video_frames |
| << " (average lateness: " |
| << (late_video_frames > 0 |
| ? static_cast<double>(total_delay_of_late_frames_ms) / |
| late_video_frames |
| : 0) |
| << " ms)"; |
| LOG(INFO) << "Average encoded bitrate (kbps): " << avg_encoded_bitrate; |
| LOG(INFO) << "Average target bitrate (kbps): " << avg_target_bitrate; |
| LOG(INFO) << "Writing log: " << log_output_path.value(); |
| |
| // Truncate file and then write serialized log. |
| { |
| base::ScopedFILE file(base::OpenFile(log_output_path, "wb")); |
| if (!file.get()) { |
| LOG(INFO) << "Cannot write to log."; |
| return; |
| } |
| } |
| AppendLogToFile(&video_metadata, video_frame_events, video_packet_events, |
| log_output_path); |
| AppendLogToFile(&audio_metadata, audio_frame_events, audio_packet_events, |
| log_output_path); |
| |
| // Write quality metrics. |
| if (quality_test) { |
| LOG(INFO) << "Writing quality metrics: " << metrics_output_path.value(); |
| std::string line; |
| for (size_t i = 0; |
| i < metrics_output.psnr.size() && i < metrics_output.ssim.size(); |
| ++i) { |
| base::StringAppendF(&line, "%f %f\n", metrics_output.psnr[i], |
| metrics_output.ssim[i]); |
| } |
| WriteFile(metrics_output_path, line.data(), line.length()); |
| } |
| } |
| |
| NetworkSimulationModel DefaultModel() { |
| NetworkSimulationModel model; |
| model.set_type(cast::proto::INTERRUPTED_POISSON_PROCESS); |
| IPPModel* ipp = model.mutable_ipp(); |
| ipp->set_coef_burstiness(0.609); |
| ipp->set_coef_variance(4.1); |
| |
| ipp->add_average_rate(0.609); |
| ipp->add_average_rate(0.495); |
| ipp->add_average_rate(0.561); |
| ipp->add_average_rate(0.458); |
| ipp->add_average_rate(0.538); |
| ipp->add_average_rate(0.513); |
| ipp->add_average_rate(0.585); |
| ipp->add_average_rate(0.592); |
| ipp->add_average_rate(0.658); |
| ipp->add_average_rate(0.556); |
| ipp->add_average_rate(0.371); |
| ipp->add_average_rate(0.595); |
| ipp->add_average_rate(0.490); |
| ipp->add_average_rate(0.980); |
| ipp->add_average_rate(0.781); |
| ipp->add_average_rate(0.463); |
| |
| return model; |
| } |
| |
| bool IsModelValid(const NetworkSimulationModel& model) { |
| if (!model.has_type()) return false; |
| NetworkSimulationModelType type = model.type(); |
| if (type == media::cast::proto::INTERRUPTED_POISSON_PROCESS) { |
| if (!model.has_ipp()) return false; |
| const IPPModel& ipp = model.ipp(); |
| if (ipp.coef_burstiness() <= 0.0 || ipp.coef_variance() <= 0.0) |
| return false; |
| if (ipp.average_rate_size() == 0) return false; |
| for (int i = 0; i < ipp.average_rate_size(); i++) { |
| if (ipp.average_rate(i) <= 0.0) return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| NetworkSimulationModel LoadModel(const base::FilePath& model_path) { |
| if (base::CommandLine::ForCurrentProcess()->HasSwitch(kNoSimulation)) { |
| NetworkSimulationModel model; |
| model.set_type(media::cast::proto::NO_SIMULATION); |
| return model; |
| } |
| if (model_path.empty()) { |
| LOG(ERROR) << "Model path not set; Using default model."; |
| return DefaultModel(); |
| } |
| std::string model_str; |
| if (!base::ReadFileToString(model_path, &model_str)) { |
| LOG(ERROR) << "Failed to read model file."; |
| return DefaultModel(); |
| } |
| |
| NetworkSimulationModel model; |
| if (!model.ParseFromString(model_str)) { |
| LOG(ERROR) << "Failed to parse model."; |
| return DefaultModel(); |
| } |
| if (!IsModelValid(model)) { |
| LOG(ERROR) << "Invalid model."; |
| return DefaultModel(); |
| } |
| |
| return model; |
| } |
| |
| } // namespace |
| } // namespace cast |
| } // namespace media |
| } // namespace cobalt |
| |
| int main(int argc, char** argv) { |
| base::AtExitManager at_exit; |
| base::CommandLine::Init(argc, argv); |
| InitLogging(logging::LoggingSettings()); |
| |
| const base::CommandLine* cmd = base::CommandLine::ForCurrentProcess(); |
| base::FilePath media_path = cmd->GetSwitchValuePath(media::cast::kLibDir); |
| if (media_path.empty()) { |
| if (!PathService::Get(base::DIR_MODULE, &media_path)) { |
| LOG(ERROR) << "Failed to load FFmpeg."; |
| return 1; |
| } |
| } |
| |
| media::InitializeMediaLibrary(); |
| |
| base::FilePath source_path = |
| cmd->GetSwitchValuePath(media::cast::kSourcePath); |
| base::FilePath log_output_path = |
| cmd->GetSwitchValuePath(media::cast::kOutputPath); |
| if (log_output_path.empty()) { |
| base::GetTempDir(&log_output_path); |
| log_output_path = log_output_path.AppendASCII("sim-events.gz"); |
| } |
| base::FilePath metrics_output_path = |
| cmd->GetSwitchValuePath(media::cast::kMetricsOutputPath); |
| base::FilePath yuv_output_path = |
| cmd->GetSwitchValuePath(media::cast::kYuvOutputPath); |
| std::string sim_id = cmd->GetSwitchValueASCII(media::cast::kSimulationId); |
| |
| NetworkSimulationModel model = |
| media::cast::LoadModel(cmd->GetSwitchValuePath(media::cast::kModelPath)); |
| |
| base::DictionaryValue values; |
| values.SetBoolean("sim", true); |
| values.SetString("sim-id", sim_id); |
| |
| std::string extra_data; |
| base::JSONWriter::Write(values, &extra_data); |
| |
| // Run. |
| media::cast::RunSimulation(source_path, log_output_path, metrics_output_path, |
| yuv_output_path, extra_data, model); |
| return 0; |
| } |