| // 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. |
| // |
| // Output: |
| // - Raw event log of the simulation session tagged with the unique test ID, |
| // written out to the specified file path. |
| |
| #include "base/at_exit.h" |
| #include "base/base_paths.h" |
| #include "base/command_line.h" |
| #include "base/file_util.h" |
| #include "base/files/file_path.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/path_service.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "base/time/tick_clock.h" |
| #include "base/values.h" |
| #include "media/base/audio_bus.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_config.h" |
| #include "media/cast/net/cast_transport_defines.h" |
| #include "media/cast/net/cast_transport_sender.h" |
| #include "media/cast/net/cast_transport_sender_impl.h" |
| #include "media/cast/test/fake_media_source.h" |
| #include "media/cast/test/fake_single_thread_task_runner.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" |
| |
| using media::cast::proto::IPPModel; |
| using media::cast::proto::NetworkSimulationModel; |
| using media::cast::proto::NetworkSimulationModelType; |
| |
| namespace media { |
| namespace cast { |
| namespace { |
| const int kTargetDelay = 300; |
| const char kSourcePath[] = "source"; |
| const char kModelPath[] = "model"; |
| const char kOutputPath[] = "output"; |
| const char kSimulationId[] = "sim-id"; |
| const char kLibDir[] = "lib-dir"; |
| |
| void UpdateCastTransportStatus(CastTransportStatus status) { |
| LOG(INFO) << "Cast transport status: " << status; |
| } |
| |
| void AudioInitializationStatus(CastInitializationStatus status) { |
| LOG(INFO) << "Audio status: " << status; |
| } |
| |
| void VideoInitializationStatus(CastInitializationStatus status) { |
| LOG(INFO) << "Video status: " << status; |
| } |
| |
| void LogTransportEvents(const scoped_refptr<CastEnvironment>& env, |
| const std::vector<PacketEvent>& packet_events) { |
| for (std::vector<media::cast::PacketEvent>::const_iterator it = |
| packet_events.begin(); |
| it != packet_events.end(); |
| ++it) { |
| env->Logging()->InsertPacketEvent(it->timestamp, |
| it->type, |
| it->media_type, |
| it->rtp_timestamp, |
| it->frame_id, |
| it->packet_id, |
| it->max_packet_id, |
| it->size); |
| } |
| } |
| |
| void GotVideoFrame( |
| int* counter, |
| CastReceiver* cast_receiver, |
| const scoped_refptr<media::VideoFrame>& video_frame, |
| const base::TimeTicks& render_time, |
| bool continuous) { |
| ++*counter; |
| cast_receiver->RequestDecodedVideoFrame( |
| base::Bind(&GotVideoFrame, counter, cast_receiver)); |
| } |
| |
| void GotAudioFrame( |
| int* counter, |
| CastReceiver* cast_receiver, |
| scoped_ptr<AudioBus> audio_bus, |
| const base::TimeTicks& playout_time, |
| bool is_continuous) { |
| ++*counter; |
| cast_receiver->RequestDecodedAudioFrame( |
| base::Bind(&GotAudioFrame, counter, cast_receiver)); |
| } |
| |
| void AppendLog(EncodingEventSubscriber* subscriber, |
| const std::string& extra_data, |
| const base::FilePath& output_path) { |
| media::cast::proto::LogMetadata metadata; |
| metadata.set_extra_data(extra_data); |
| |
| media::cast::FrameEventList frame_events; |
| media::cast::PacketEventList packet_events; |
| subscriber->GetEventsAndReset( |
| &metadata, &frame_events, &packet_events); |
| media::cast::proto::GeneralDescription* gen_desc = |
| metadata.mutable_general_description(); |
| gen_desc->set_product("Cast Simulator"); |
| gen_desc->set_product_version("0.1"); |
| |
| scoped_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) == -1) { |
| LOG(ERROR) << "Failed to append to log."; |
| } |
| } |
| |
| // Run simulation once. |
| // |
| // |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& 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<test::FakeSingleThreadTaskRunner> task_runner = |
| new test::FakeSingleThreadTaskRunner(&testing_clock); |
| base::ThreadTaskRunnerHandle task_runner_handle(task_runner); |
| |
| // CastEnvironments. |
| scoped_refptr<CastEnvironment> sender_env = |
| new CastEnvironment( |
| scoped_ptr<base::TickClock>( |
| new test::SkewedTickClock(&testing_clock)).Pass(), |
| task_runner, |
| task_runner, |
| task_runner); |
| scoped_refptr<CastEnvironment> receiver_env = |
| new CastEnvironment( |
| scoped_ptr<base::TickClock>( |
| new test::SkewedTickClock(&testing_clock)).Pass(), |
| 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->Logging()->AddRawEventSubscriber(&audio_event_subscriber); |
| sender_env->Logging()->AddRawEventSubscriber(&video_event_subscriber); |
| |
| // Audio sender config. |
| AudioSenderConfig audio_sender_config = GetDefaultAudioSenderConfig(); |
| audio_sender_config.target_playout_delay = |
| base::TimeDelta::FromMilliseconds(kTargetDelay); |
| |
| // Audio receiver config. |
| FrameReceiverConfig audio_receiver_config = |
| GetDefaultAudioReceiverConfig(); |
| audio_receiver_config.rtp_max_delay_ms = |
| audio_sender_config.target_playout_delay.InMilliseconds(); |
| |
| // Video sender config. |
| VideoSenderConfig video_sender_config = GetDefaultVideoSenderConfig(); |
| video_sender_config.max_bitrate = 4000000; |
| video_sender_config.min_bitrate = 2000000; |
| video_sender_config.start_bitrate = 4000000; |
| video_sender_config.target_playout_delay = |
| base::TimeDelta::FromMilliseconds(kTargetDelay); |
| |
| // Video receiver config. |
| FrameReceiverConfig video_receiver_config = |
| GetDefaultVideoReceiverConfig(); |
| video_receiver_config.rtp_max_delay_ms = |
| video_sender_config.target_playout_delay.InMilliseconds(); |
| |
| // Loopback transport. |
| LoopBackTransport receiver_to_sender(receiver_env); |
| LoopBackTransport sender_to_receiver(sender_env); |
| |
| // Cast receiver. |
| scoped_ptr<CastReceiver> cast_receiver( |
| CastReceiver::Create(receiver_env, |
| audio_receiver_config, |
| video_receiver_config, |
| &receiver_to_sender)); |
| |
| // Cast sender and transport sender. |
| scoped_ptr<CastTransportSender> transport_sender( |
| new CastTransportSenderImpl( |
| NULL, |
| &testing_clock, |
| net::IPEndPoint(), |
| base::Bind(&UpdateCastTransportStatus), |
| base::Bind(&LogTransportEvents, sender_env), |
| base::TimeDelta::FromSeconds(1), |
| task_runner, |
| &sender_to_receiver)); |
| scoped_ptr<CastSender> cast_sender( |
| CastSender::Create(sender_env, transport_sender.get())); |
| |
| // Build packet pipe. |
| if (model.type() != media::cast::proto::INTERRUPTED_POISSON_PROCESS) { |
| LOG(ERROR) << "Unknown model type " << model.type() << "."; |
| return; |
| } |
| |
| 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()); |
| test::InterruptedPoissonProcess ipp(average_rates, |
| ipp_model.coef_burstiness(), ipp_model.coef_variance(), 0); |
| |
| // Connect sender to receiver. This initializes the pipe. |
| receiver_to_sender.Initialize( |
| ipp.NewBuffer(128 * 1024), cast_sender->packet_receiver(), task_runner, |
| &testing_clock); |
| sender_to_receiver.Initialize( |
| ipp.NewBuffer(128 * 1024), cast_receiver->packet_receiver(), task_runner, |
| &testing_clock); |
| |
| // Start receiver. |
| int audio_frame_count = 0; |
| int video_frame_count = 0; |
| cast_receiver->RequestDecodedVideoFrame( |
| base::Bind(&GotVideoFrame, &video_frame_count, cast_receiver.get())); |
| cast_receiver->RequestDecodedAudioFrame( |
| base::Bind(&GotAudioFrame, &audio_frame_count, cast_receiver.get())); |
| |
| FakeMediaSource media_source(task_runner, |
| &testing_clock, |
| video_sender_config); |
| |
| // Initializing audio and video senders. |
| cast_sender->InitializeAudio(audio_sender_config, |
| base::Bind(&AudioInitializationStatus)); |
| cast_sender->InitializeVideo(media_source.get_video_config(), |
| base::Bind(&VideoInitializationStatus), |
| CreateDefaultVideoEncodeAcceleratorCallback(), |
| CreateDefaultVideoEncodeMemoryCallback()); |
| |
| // Start sending. |
| if (!source_path.empty()) { |
| // 0 means using the FPS from the file. |
| media_source.SetSourceFile(source_path, 0); |
| } |
| media_source.Start(cast_sender->audio_frame_input(), |
| cast_sender->video_frame_input()); |
| |
| // Run for 3 minutes. |
| base::TimeDelta elapsed_time; |
| while (elapsed_time.InMinutes() < 3) { |
| // Each step is 100us. |
| base::TimeDelta step = base::TimeDelta::FromMicroseconds(100); |
| task_runner->Sleep(step); |
| elapsed_time += step; |
| } |
| |
| LOG(INFO) << "Audio frame count: " << audio_frame_count; |
| LOG(INFO) << "Video frame count: " << video_frame_count; |
| LOG(INFO) << "Writing log: " << output_path.value(); |
| |
| // Truncate file and then write serialized log. |
| { |
| base::ScopedFILE file(base::OpenFile(output_path, "wb")); |
| if (!file.get()) { |
| LOG(INFO) << "Cannot write to log."; |
| return; |
| } |
| } |
| AppendLog(&video_event_subscriber, extra_data, output_path); |
| AppendLog(&audio_event_subscriber, extra_data, output_path); |
| } |
| |
| 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 (model_path.empty()) { |
| LOG(ERROR) << "Model path not set."; |
| 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 |
| |
| int main(int argc, char** argv) { |
| base::AtExitManager at_exit; |
| CommandLine::Init(argc, argv); |
| InitLogging(logging::LoggingSettings()); |
| |
| const CommandLine* cmd = 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; |
| } |
| } |
| |
| if (!media::InitializeMediaLibrary(media_path)) { |
| LOG(ERROR) << "Failed to initialize FFmpeg."; |
| return 1; |
| } |
| |
| base::FilePath source_path = cmd->GetSwitchValuePath( |
| media::cast::kSourcePath); |
| base::FilePath output_path = cmd->GetSwitchValuePath( |
| media::cast::kOutputPath); |
| if (output_path.empty()) { |
| base::GetTempDir(&output_path); |
| output_path = output_path.AppendASCII("sim-events.gz"); |
| } |
| 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, output_path, extra_data, model); |
| return 0; |
| } |