diff options
Diffstat (limited to 'cast/standalone_receiver')
24 files changed, 2448 insertions, 0 deletions
diff --git a/cast/standalone_receiver/BUILD.gn b/cast/standalone_receiver/BUILD.gn new file mode 100644 index 00000000..74d53f65 --- /dev/null +++ b/cast/standalone_receiver/BUILD.gn @@ -0,0 +1,79 @@ +# Copyright 2019 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. + +import("//build/config/external_libraries.gni") +import("//build_overrides/build.gni") + +# Define the executable target only when the build is configured to use the +# standalone platform implementation; since this is itself a standalone +# application. +if (!build_with_chromium) { + shared_sources = [ + "cast_service.cc", + "cast_service.h", + "mirroring_application.cc", + "mirroring_application.h", + "streaming_playback_controller.cc", + "streaming_playback_controller.h", + ] + + shared_deps = [ + "../common:public", + "../streaming:receiver", + ] + + have_external_libs = have_ffmpeg && have_libsdl2 + + if (have_external_libs) { + source_set("standalone_receiver_sdl") { + sources = shared_sources + deps = shared_deps + + defines = [ "CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS" ] + sources += [ + "avcodec_glue.h", + "decoder.cc", + "decoder.h", + "sdl_audio_player.cc", + "sdl_audio_player.h", + "sdl_glue.cc", + "sdl_glue.h", + "sdl_player_base.cc", + "sdl_player_base.h", + "sdl_video_player.cc", + "sdl_video_player.h", + ] + include_dirs = ffmpeg_include_dirs + libsdl2_include_dirs + lib_dirs = ffmpeg_lib_dirs + libsdl2_lib_dirs + libs = ffmpeg_libs + libsdl2_libs + } + } + + source_set("standalone_receiver_dummy") { + sources = shared_sources + deps = shared_deps + + sources += [ + "dummy_player.cc", + "dummy_player.h", + ] + } + + executable("cast_receiver") { + sources = [ "main.cc" ] + + deps = [ + "../receiver:agent", + "../receiver:channel", + ] + + configs += [ "../common:certificate_config" ] + + if (have_external_libs) { + deps += [ ":standalone_receiver_sdl" ] + } else { + deps += [ ":standalone_receiver_dummy" ] + } + } +} diff --git a/cast/standalone_receiver/DEPS b/cast/standalone_receiver/DEPS new file mode 100644 index 00000000..f2040e13 --- /dev/null +++ b/cast/standalone_receiver/DEPS @@ -0,0 +1,10 @@ +# Copyright 2019 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_rules = [ + '+cast', + '+platform/impl', + '+discovery/common', + '+discovery/public', +] diff --git a/cast/standalone_receiver/avcodec_glue.h b/cast/standalone_receiver/avcodec_glue.h new file mode 100644 index 00000000..aa516175 --- /dev/null +++ b/cast/standalone_receiver/avcodec_glue.h @@ -0,0 +1,56 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_AVCODEC_GLUE_H_ +#define CAST_STANDALONE_RECEIVER_AVCODEC_GLUE_H_ + +#include <memory> + +extern "C" { +#include <libavcodec/avcodec.h> +#include <libavutil/common.h> +#include <libavutil/imgutils.h> +#include <libavutil/samplefmt.h> +} + +namespace openscreen { +namespace cast { + +// Macro that, for an AVFoo, generates code for: +// +// using FooUniquePtr = std::unique_ptr<Foo, FooFreer>; +// FooUniquePtr MakeUniqueFoo(...args...); +#define DEFINE_AV_UNIQUE_PTR(name, create_func, free_statement) \ + namespace internal { \ + struct name##Freer { \ + void operator()(name* obj) const { \ + if (obj) { \ + free_statement; \ + } \ + } \ + }; \ + } \ + \ + using name##UniquePtr = std::unique_ptr<name, internal::name##Freer>; \ + \ + template <typename... Args> \ + name##UniquePtr MakeUnique##name(Args&&... args) { \ + return name##UniquePtr(create_func(std::forward<Args>(args)...)); \ + } + +DEFINE_AV_UNIQUE_PTR(AVCodecParserContext, + av_parser_init, + av_parser_close(obj)); +DEFINE_AV_UNIQUE_PTR(AVCodecContext, + avcodec_alloc_context3, + avcodec_free_context(&obj)); +DEFINE_AV_UNIQUE_PTR(AVPacket, av_packet_alloc, av_packet_free(&obj)); +DEFINE_AV_UNIQUE_PTR(AVFrame, av_frame_alloc, av_frame_free(&obj)); + +#undef DEFINE_AV_UNIQUE_PTR + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_AVCODEC_GLUE_H_ diff --git a/cast/standalone_receiver/cast_service.cc b/cast/standalone_receiver/cast_service.cc new file mode 100644 index 00000000..75790197 --- /dev/null +++ b/cast/standalone_receiver/cast_service.cc @@ -0,0 +1,109 @@ +// 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_receiver/cast_service.h" + +#include <utility> + +#include "discovery/common/config.h" +#include "platform/api/tls_connection_factory.h" +#include "platform/base/interface_info.h" +#include "platform/base/tls_listen_options.h" +#include "util/osp_logging.h" +#include "util/stringprintf.h" + +namespace openscreen { +namespace cast { + +namespace { + +constexpr uint16_t kDefaultCastServicePort = 8010; + +constexpr int kDefaultMaxBacklogSize = 64; +const TlsListenOptions kDefaultListenOptions{kDefaultMaxBacklogSize}; + +IPEndpoint DetermineEndpoint(const InterfaceInfo& interface) { + const IPAddress address = interface.GetIpAddressV4() + ? interface.GetIpAddressV4() + : interface.GetIpAddressV6(); + OSP_CHECK(address); + return IPEndpoint{address, kDefaultCastServicePort}; +} + +discovery::Config MakeDiscoveryConfig(const InterfaceInfo& interface) { + discovery::Config config; + + discovery::Config::NetworkInfo::AddressFamilies supported_address_families = + discovery::Config::NetworkInfo::kNoAddressFamily; + if (interface.GetIpAddressV4()) { + supported_address_families |= discovery::Config::NetworkInfo::kUseIpV4; + } else if (interface.GetIpAddressV6()) { + supported_address_families |= discovery::Config::NetworkInfo::kUseIpV6; + } + config.network_info.push_back({interface, supported_address_families}); + + return config; +} + +} // namespace + +CastService::CastService(TaskRunner* task_runner, + const InterfaceInfo& interface, + GeneratedCredentials credentials, + const std::string& friendly_name, + const std::string& model_name, + bool enable_discovery) + : local_endpoint_(DetermineEndpoint(interface)), + credentials_(std::move(credentials)), + agent_(task_runner, credentials_.provider.get()), + mirroring_application_(task_runner, local_endpoint_.address, &agent_), + socket_factory_(&agent_, agent_.cast_socket_client()), + connection_factory_( + TlsConnectionFactory::CreateFactory(&socket_factory_, task_runner)), + discovery_service_(enable_discovery ? discovery::CreateDnsSdService( + task_runner, + this, + MakeDiscoveryConfig(interface)) + : LazyDeletedDiscoveryService()), + discovery_publisher_( + discovery_service_ + ? MakeSerialDelete<discovery::DnsSdServicePublisher<ServiceInfo>>( + task_runner, + discovery_service_.get(), + kCastV2ServiceId, + ServiceInfoToDnsSdInstance) + : LazyDeletedDiscoveryPublisher()) { + connection_factory_->SetListenCredentials(credentials_.tls_credentials); + connection_factory_->Listen(local_endpoint_, kDefaultListenOptions); + + if (discovery_publisher_) { + ServiceInfo info; + info.port = local_endpoint_.port; + info.unique_id = HexEncode(interface.hardware_address); + info.friendly_name = friendly_name; + info.model_name = model_name; + info.capabilities = kHasVideoOutput | kHasAudioOutput; + Error error = discovery_publisher_->Register(info); + if (!error.ok()) { + OnFatalError(std::move(error)); + } + } +} + +CastService::~CastService() { + if (discovery_publisher_) { + discovery_publisher_->DeregisterAll(); + } +} + +void CastService::OnFatalError(Error error) { + OSP_LOG_FATAL << "Encountered fatal discovery error: " << error; +} + +void CastService::OnRecoverableError(Error error) { + OSP_LOG_ERROR << "Encountered recoverable discovery error: " << error; +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/cast_service.h b/cast/standalone_receiver/cast_service.h new file mode 100644 index 00000000..99137de2 --- /dev/null +++ b/cast/standalone_receiver/cast_service.h @@ -0,0 +1,77 @@ +// 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. + +#ifndef CAST_STANDALONE_RECEIVER_CAST_SERVICE_H_ +#define CAST_STANDALONE_RECEIVER_CAST_SERVICE_H_ + +#include <memory> +#include <string> + +#include "cast/common/public/service_info.h" +#include "cast/receiver/application_agent.h" +#include "cast/receiver/channel/static_credentials.h" +#include "cast/receiver/public/receiver_socket_factory.h" +#include "cast/standalone_receiver/mirroring_application.h" +#include "discovery/common/reporting_client.h" +#include "discovery/public/dns_sd_service_factory.h" +#include "discovery/public/dns_sd_service_publisher.h" +#include "platform/api/serial_delete_ptr.h" +#include "platform/base/error.h" +#include "platform/base/ip_address.h" + +namespace openscreen { + +struct InterfaceInfo; +class TaskRunner; +class TlsConnectionFactory; + +namespace cast { + +// Assembles all the necessary components and manages their lifetimes, to create +// a full Cast Receiver on the network, with the following overall +// functionality: +// +// * Listens for TCP connections on port 8010. +// * Establishes TLS tunneling over those connections. +// * Wraps a CastSocket API around the TLS connections. +// * Manages available receiver-side applications. +// * Provides a Cast V2 Mirroring application (media streaming playback in an +// on-screen window). +// * Publishes over mDNS to be discoverable to all senders on the same LAN. +class CastService final : public discovery::ReportingClient { + public: + CastService(TaskRunner* task_runner, + const InterfaceInfo& interface, + GeneratedCredentials credentials, + const std::string& friendly_name, + const std::string& model_name, + bool enable_discovery = true); + + ~CastService() final; + + private: + using LazyDeletedDiscoveryService = SerialDeletePtr<discovery::DnsSdService>; + using LazyDeletedDiscoveryPublisher = + SerialDeletePtr<discovery::DnsSdServicePublisher<ServiceInfo>>; + + // discovery::ReportingClient overrides. + void OnFatalError(Error error) final; + void OnRecoverableError(Error error) final; + + const IPEndpoint local_endpoint_; + const GeneratedCredentials credentials_; + + ApplicationAgent agent_; + MirroringApplication mirroring_application_; + ReceiverSocketFactory socket_factory_; + std::unique_ptr<TlsConnectionFactory> connection_factory_; + + LazyDeletedDiscoveryService discovery_service_; + LazyDeletedDiscoveryPublisher discovery_publisher_; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_CAST_SERVICE_H_ diff --git a/cast/standalone_receiver/decoder.cc b/cast/standalone_receiver/decoder.cc new file mode 100644 index 00000000..9a2324e3 --- /dev/null +++ b/cast/standalone_receiver/decoder.cc @@ -0,0 +1,221 @@ +// Copyright 2019 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_receiver/decoder.h" + +#include <algorithm> +#include <sstream> +#include <thread> + +#include "util/osp_logging.h" +#include "util/trace_logging.h" + +namespace openscreen { +namespace cast { + +Decoder::Buffer::Buffer() { + Resize(0); +} + +Decoder::Buffer::~Buffer() = default; + +void Decoder::Buffer::Resize(int new_size) { + const int padded_size = new_size + AV_INPUT_BUFFER_PADDING_SIZE; + if (static_cast<int>(buffer_.size()) == padded_size) { + return; + } + buffer_.resize(padded_size); + // libavcodec requires zero-padding the region at the end, as some decoders + // will treat this as a stop marker. + memset(buffer_.data() + new_size, 0, AV_INPUT_BUFFER_PADDING_SIZE); +} + +absl::Span<const uint8_t> Decoder::Buffer::GetSpan() const { + return absl::Span<const uint8_t>( + buffer_.data(), buffer_.size() - AV_INPUT_BUFFER_PADDING_SIZE); +} + +absl::Span<uint8_t> Decoder::Buffer::GetSpan() { + return absl::Span<uint8_t>(buffer_.data(), + buffer_.size() - AV_INPUT_BUFFER_PADDING_SIZE); +} + +Decoder::Client::Client() = default; +Decoder::Client::~Client() = default; + +Decoder::Decoder(const std::string& codec_name) : codec_name_(codec_name) {} + +Decoder::~Decoder() = default; + +void Decoder::Decode(FrameId frame_id, const Decoder::Buffer& buffer) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + if (!codec_ && !Initialize()) { + return; + } + + // Parse the buffer for the required metadata and the packet to send to the + // decoder. + const absl::Span<const uint8_t> input = buffer.GetSpan(); + const int bytes_consumed = av_parser_parse2( + parser_.get(), context_.get(), &packet_->data, &packet_->size, + input.data(), input.size(), AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0); + if (bytes_consumed < 0) { + OnError("av_parser_parse2", bytes_consumed, frame_id); + return; + } + if (!packet_->data) { + OnError("av_parser_parse2 found no packet", AVERROR_BUFFER_TOO_SMALL, + frame_id); + return; + } + + // Send the packet to the decoder. + const int send_packet_result = + avcodec_send_packet(context_.get(), packet_.get()); + if (send_packet_result < 0) { + // The result should not be EAGAIN because this code always pulls out all + // the decoded frames after feeding-in each AVPacket. + OSP_DCHECK_NE(send_packet_result, AVERROR(EAGAIN)); + OnError("avcodec_send_packet", send_packet_result, frame_id); + return; + } + frames_decoding_.push_back(frame_id); + + // Receive zero or more frames from the decoder. + for (;;) { + const int receive_frame_result = + avcodec_receive_frame(context_.get(), decoded_frame_.get()); + if (receive_frame_result == AVERROR(EAGAIN)) { + break; // Decoder needs more input to produce another frame. + } + const FrameId decoded_frame_id = DidReceiveFrameFromDecoder(); + if (receive_frame_result < 0) { + OnError("avcodec_receive_frame", receive_frame_result, decoded_frame_id); + return; + } + if (client_) { + client_->OnFrameDecoded(decoded_frame_id, *decoded_frame_); + } + av_frame_unref(decoded_frame_.get()); + } +} + +bool Decoder::Initialize() { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + // NOTE: The codec_name values found in OFFER messages, such as "vp8" or + // "h264" or "opus" are valid input strings to FFMPEG's look-up function, so + // no translation is required here. + codec_ = avcodec_find_decoder_by_name(codec_name_.c_str()); + if (!codec_) { + HandleInitializationError("codec not available", AVERROR(EINVAL)); + return false; + } + OSP_LOG_INFO << "Found codec: " << codec_name_ << " (known to FFMPEG as " + << avcodec_get_name(codec_->id) << ')'; + + parser_ = MakeUniqueAVCodecParserContext(codec_->id); + if (!parser_) { + HandleInitializationError("failed to allocate parser context", + AVERROR(ENOMEM)); + return false; + } + + context_ = MakeUniqueAVCodecContext(codec_); + if (!context_) { + HandleInitializationError("failed to allocate codec context", + AVERROR(ENOMEM)); + return false; + } + + // This should always be greater than zero, so that decoding doesn't block the + // main thread of this receiver app and cause playback timing issues. The + // actual number should be tuned, based on the number of CPU cores. + // + // This should also be 16 or less, since the encoder implementations emit + // warnings about too many encode threads. FFMPEG's VP8 implementation + // actually silently freezes if this is 10 or more. Thus, 8 is used for the + // max here, just to be safe. + context_->thread_count = + std::min(std::max<int>(std::thread::hardware_concurrency(), 1), 8); + const int open_result = avcodec_open2(context_.get(), codec_, nullptr); + if (open_result < 0) { + HandleInitializationError("failed to open codec", open_result); + return false; + } + + packet_ = MakeUniqueAVPacket(); + if (!packet_) { + HandleInitializationError("failed to allocate AVPacket", AVERROR(ENOMEM)); + return false; + } + + decoded_frame_ = MakeUniqueAVFrame(); + if (!decoded_frame_) { + HandleInitializationError("failed to allocate AVFrame", AVERROR(ENOMEM)); + return false; + } + + return true; +} + +FrameId Decoder::DidReceiveFrameFromDecoder() { + const auto it = frames_decoding_.begin(); + OSP_DCHECK(it != frames_decoding_.end()); + const auto frame_id = *it; + frames_decoding_.erase(it); + return frame_id; +} + +void Decoder::HandleInitializationError(const char* what, int av_errnum) { + // If the codec was found, get FFMPEG's canonical name for it. + const char* const canonical_name = + codec_ ? avcodec_get_name(codec_->id) : nullptr; + + codec_ = nullptr; // Set null to mean "not initialized." + + if (!client_) { + return; // Nowhere to emit error to, so don't bother. + } + + std::ostringstream error; + error << "Could not initialize codec " << codec_name_; + if (canonical_name) { + error << " (known to FFMPEG as " << canonical_name << ')'; + } + error << " because " << what << " (" << av_err2str(av_errnum) << ")."; + client_->OnFatalError(error.str()); +} + +void Decoder::OnError(const char* what, int av_errnum, FrameId frame_id) { + if (!client_) { + return; + } + + // Make a human-readable string from the libavcodec error. + std::ostringstream error; + if (!frame_id.is_null()) { + error << "frame: " << frame_id << "; "; + } + + char human_readable_error[AV_ERROR_MAX_STRING_SIZE]{0}; + av_make_error_string(human_readable_error, AV_ERROR_MAX_STRING_SIZE, + av_errnum); + error << "what: " << what << "; error: " << human_readable_error; + + // Dispatch to either the fatal error handler, or the one for decode errors, + // as appropriate. + switch (av_errnum) { + case AVERROR_EOF: + case AVERROR(EINVAL): + case AVERROR(ENOMEM): + client_->OnFatalError(error.str()); + break; + default: + client_->OnDecodeError(frame_id, error.str()); + break; + } +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/decoder.h b/cast/standalone_receiver/decoder.h new file mode 100644 index 00000000..1d4d0791 --- /dev/null +++ b/cast/standalone_receiver/decoder.h @@ -0,0 +1,99 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_DECODER_H_ +#define CAST_STANDALONE_RECEIVER_DECODER_H_ + +#include <stdint.h> + +#include <string> +#include <vector> + +#include "absl/types/span.h" +#include "cast/standalone_receiver/avcodec_glue.h" +#include "cast/streaming/frame_id.h" + +namespace openscreen { +namespace cast { + +// Wraps libavcodec to decode audio or video. +class Decoder { + public: + // A buffer backed by storage that is compatible with FFMPEG (i.e., includes + // the required zero-padding). + class Buffer { + public: + Buffer(); + ~Buffer(); + + void Resize(int new_size); + absl::Span<const uint8_t> GetSpan() const; + absl::Span<uint8_t> GetSpan(); + + private: + std::vector<uint8_t> buffer_; + }; + + // Interface for receiving decoded frames and/or errors. + class Client { + public: + virtual ~Client(); + + virtual void OnFrameDecoded(FrameId frame_id, const AVFrame& frame) = 0; + virtual void OnDecodeError(FrameId frame_id, std::string message) = 0; + virtual void OnFatalError(std::string message) = 0; + + protected: + Client(); + }; + + // |codec_name| should be the codec_name field from an OFFER message. + explicit Decoder(const std::string& codec_name); + ~Decoder(); + + Client* client() const { return client_; } + void set_client(Client* client) { client_ = client; } + + // Starts decoding the data in |buffer|, which should be associated with the + // given |frame_id|. This will synchronously call Client::OnFrameDecoded() + // and/or Client::OnDecodeError() zero or more times with results. Note that + // some codecs will have data dependencies that require multiple encoded + // frame's data before the first decoded frame can be generated. + void Decode(FrameId frame_id, const Buffer& buffer); + + private: + // Helper to initialize the FFMPEG decoder and supporting objects. Returns + // false if this failed (and the Client was notified). + bool Initialize(); + + // Helper to get the FrameId that is associated with the next frame coming out + // of the FFMPEG decoder. + FrameId DidReceiveFrameFromDecoder(); + + // Helper to handle a codec initialization error and notify the Client of the + // fatal error. + void HandleInitializationError(const char* what, int av_errnum); + + // Called when any transient or fatal error occurs, generating an Error and + // notifying the Client of it. + void OnError(const char* what, int av_errnum, FrameId frame_id); + + const std::string codec_name_; + AVCodec* codec_ = nullptr; + AVCodecParserContextUniquePtr parser_; + AVCodecContextUniquePtr context_; + AVPacketUniquePtr packet_; + AVFrameUniquePtr decoded_frame_; + + Client* client_ = nullptr; + + // Queue of frames that have been input to the libavcodec decoder, but which + // have not yet had output generated by it. + std::vector<FrameId> frames_decoding_; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_DECODER_H_ diff --git a/cast/standalone_receiver/dummy_player.cc b/cast/standalone_receiver/dummy_player.cc new file mode 100644 index 00000000..ce54d82f --- /dev/null +++ b/cast/standalone_receiver/dummy_player.cc @@ -0,0 +1,44 @@ +// Copyright 2019 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_receiver/dummy_player.h" + +#include <chrono> + +#include "absl/types/span.h" +#include "cast/streaming/encoded_frame.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" + +namespace openscreen { +namespace cast { + +DummyPlayer::DummyPlayer(Receiver* receiver) : receiver_(receiver) { + OSP_DCHECK(receiver_); + receiver_->SetConsumer(this); +} + +DummyPlayer::~DummyPlayer() { + receiver_->SetConsumer(nullptr); +} + +void DummyPlayer::OnFramesReady(int buffer_size) { + // Consume the next frame. + buffer_.resize(buffer_size); + const EncodedFrame frame = + receiver_->ConsumeNextFrame(absl::Span<uint8_t>(buffer_)); + + // Convert the RTP timestamp to a human-readable timestamp (in µs) and log + // some short information about the frame. + const auto media_timestamp = + frame.rtp_timestamp.ToTimeSinceOrigin<microseconds>( + receiver_->rtp_timebase()); + OSP_LOG_INFO << "[SSRC " << receiver_->ssrc() << "] " + << (frame.dependency == EncodedFrame::KEY_FRAME ? "KEY " : "") + << frame.frame_id << " at " << media_timestamp.count() << "µs, " + << buffer_size << " bytes"; +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/dummy_player.h b/cast/standalone_receiver/dummy_player.h new file mode 100644 index 00000000..e8db1bf7 --- /dev/null +++ b/cast/standalone_receiver/dummy_player.h @@ -0,0 +1,40 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_DUMMY_PLAYER_H_ +#define CAST_STANDALONE_RECEIVER_DUMMY_PLAYER_H_ + +#include <stdint.h> + +#include <vector> + +#include "cast/streaming/receiver.h" +#include "platform/api/task_runner.h" +#include "platform/api/time.h" + +namespace openscreen { +namespace cast { + +// Consumes frames from a Receiver, but does nothing other than OSP_LOG_INFO +// each one's FrameId, timestamp and size. This is only useful for confirming a +// Receiver is successfully receiving a stream, for platforms where +// SDLVideoPlayer cannot be built. +class DummyPlayer final : public Receiver::Consumer { + public: + explicit DummyPlayer(Receiver* receiver); + + ~DummyPlayer() final; + + private: + // Receiver::Consumer implementation. + void OnFramesReady(int next_frame_buffer_size) final; + + Receiver* const receiver_; + std::vector<uint8_t> buffer_; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_DUMMY_PLAYER_H_ diff --git a/cast/standalone_receiver/install_demo_deps_debian.sh b/cast/standalone_receiver/install_demo_deps_debian.sh new file mode 100755 index 00000000..c082455c --- /dev/null +++ b/cast/standalone_receiver/install_demo_deps_debian.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +# Installs dependencies necessary for libSDL and libAVcodec on Debian systems. + +sudo apt-get install libsdl2-2.0 libsdl2-dev libavcodec libavcodec-dev \ + libavformat libavformat-dev libavutil libavutil-dev \ + libswresample libswresample-dev
\ No newline at end of file diff --git a/cast/standalone_receiver/install_demo_deps_raspian.sh b/cast/standalone_receiver/install_demo_deps_raspian.sh new file mode 100755 index 00000000..91acaaa6 --- /dev/null +++ b/cast/standalone_receiver/install_demo_deps_raspian.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh + +# Installs dependencies necessary for libSDL and libAVcodec on +# Raspberry PI units running Raspian. + +sudo apt-get install libavcodec58=7:4.1.4* libavcodec-dev=7:4.1.4* \ + libsdl2-2.0-0=2.0.9* libsdl2-dev=2.0.9* \ + libavformat-dev=7:4.1.4* diff --git a/cast/standalone_receiver/main.cc b/cast/standalone_receiver/main.cc new file mode 100644 index 00000000..9e305c8b --- /dev/null +++ b/cast/standalone_receiver/main.cc @@ -0,0 +1,248 @@ +// Copyright 2019 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 <getopt.h> + +#include <algorithm> +#include <iostream> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "absl/strings/str_cat.h" +#include "cast/receiver/channel/static_credentials.h" +#include "cast/standalone_receiver/cast_service.h" +#include "platform/api/time.h" +#include "platform/base/error.h" +#include "platform/base/ip_address.h" +#include "platform/impl/logging.h" +#include "platform/impl/network_interface.h" +#include "platform/impl/platform_client_posix.h" +#include "platform/impl/task_runner.h" +#include "platform/impl/text_trace_logging_platform.h" +#include "util/chrono_helpers.h" +#include "util/stringprintf.h" +#include "util/trace_logging.h" + +namespace openscreen { +namespace cast { +namespace { + +void LogUsage(const char* argv0) { + constexpr char kTemplate[] = R"( +usage: %s <options> <interface> + + interface + Specifies the network interface to bind to. The interface is + looked up from the system interface registry. + Mandatory, as it must be known for publishing discovery. + +options: + -p, --private-key=path-to-key: Path to OpenSSL-generated private key to be + used for TLS authentication. If a private key is not + provided, a randomly generated one will be used for this + session. + + -d, --developer-certificate=path-to-cert: Path to PEM file containing a + developer generated server root TLS certificate. + If a root server certificate is not provided, one + will be generated using a randomly generated + private key. Note that if a certificate path is + passed, the private key path is a mandatory field. + + -g, --generate-credentials: Instructs the binary to generate a private key + and self-signed root certificate with the CA + bit set to true, and then exit. The resulting + private key and certificate can then be used as + values for the -p and -s flags. + + -f, --friendly-name: Friendly name to be used for device discovery. + + -m, --model-name: Model name to be used for device discovery. + + -t, --tracing: Enable performance tracing logging. + + -v, --verbose: Enable verbose logging. + + -h, --help: Show this help message. +)"; + + std::cerr << StringPrintf(kTemplate, argv0); +} + +InterfaceInfo GetInterfaceInfoFromName(const char* name) { + OSP_CHECK(name != nullptr) << "Missing mandatory argument: interface."; + InterfaceInfo interface_info; + std::vector<InterfaceInfo> network_interfaces = GetNetworkInterfaces(); + for (auto& interface : network_interfaces) { + if (interface.name == name) { + interface_info = std::move(interface); + break; + } + } + + if (interface_info.name.empty()) { + auto error_or_info = GetLoopbackInterfaceForTesting(); + if (error_or_info.has_value()) { + if (error_or_info.value().name == name) { + interface_info = std::move(error_or_info.value()); + } + } + } + OSP_CHECK(!interface_info.name.empty()) << "Invalid interface specified."; + return interface_info; +} + +void RunCastService(TaskRunnerImpl* task_runner, + const InterfaceInfo& interface, + GeneratedCredentials creds, + const std::string& friendly_name, + const std::string& model_name, + bool discovery_enabled) { + std::unique_ptr<CastService> service; + task_runner->PostTask([&] { + service = std::make_unique<CastService>(task_runner, interface, + std::move(creds), friendly_name, + model_name, discovery_enabled); + }); + + OSP_LOG_INFO << "CastService is running. CTRL-C (SIGINT), or send a " + "SIGTERM to exit."; + task_runner->RunUntilSignaled(); + + // Spin the TaskRunner to execute destruction/shutdown tasks. + OSP_LOG_INFO << "Shutting down..."; + task_runner->PostTask([&] { + service.reset(); + task_runner->RequestStopSoon(); + }); + task_runner->RunUntilStopped(); + OSP_LOG_INFO << "Bye!"; +} + +int RunStandaloneReceiver(int argc, char* argv[]) { +#if !defined(CAST_ALLOW_DEVELOPER_CERTIFICATE) + OSP_LOG_FATAL + << "It compiled! However cast_receiver currently only supports using a " + "passed-in certificate and private key, and must be built with " + "cast_allow_developer_certificate=true set in the GN args to " + "actually do anything interesting."; + return 1; +#endif + + // A note about modifying command line arguments: consider uniformity + // between all Open Screen executables. If it is a platform feature + // being exposed, consider if it applies to the standalone receiver, + // standalone sender, osp demo, and test_main argument options. + const struct option kArgumentOptions[] = { + {"private-key", required_argument, nullptr, 'p'}, + {"developer-certificate", required_argument, nullptr, 'd'}, + {"generate-credentials", no_argument, nullptr, 'g'}, + {"friendly-name", required_argument, nullptr, 'f'}, + {"model-name", required_argument, nullptr, 'm'}, + {"tracing", no_argument, nullptr, 't'}, + {"verbose", no_argument, nullptr, 'v'}, + {"help", no_argument, nullptr, 'h'}, + + // Discovery is enabled by default, however there are cases where it + // needs to be disabled, such as on Mac OS X. + {"disable-discovery", no_argument, nullptr, 'x'}, + {nullptr, 0, nullptr, 0}}; + + bool is_verbose = false; + bool discovery_enabled = true; + std::string private_key_path; + std::string developer_certificate_path; + std::string friendly_name = "Cast Standalone Receiver"; + std::string model_name = "cast_standalone_receiver"; + bool should_generate_credentials = false; + std::unique_ptr<TextTraceLoggingPlatform> trace_logger; + int ch = -1; + while ((ch = getopt_long(argc, argv, "p:d:f:m:gtvhx", kArgumentOptions, + nullptr)) != -1) { + switch (ch) { + case 'p': + private_key_path = optarg; + break; + case 'd': + developer_certificate_path = optarg; + break; + case 'f': + friendly_name = optarg; + break; + case 'm': + model_name = optarg; + break; + case 'g': + should_generate_credentials = true; + break; + case 't': + trace_logger = std::make_unique<TextTraceLoggingPlatform>(); + break; + case 'v': + is_verbose = true; + break; + case 'x': + discovery_enabled = false; + break; + case 'h': + LogUsage(argv[0]); + return 1; + } + } + + SetLogLevel(is_verbose ? LogLevel::kVerbose : LogLevel::kInfo); + + // Either -g is required, or both -p and -d. + if (should_generate_credentials) { + GenerateDeveloperCredentialsToFile(); + return 0; + } + if (private_key_path.empty() || developer_certificate_path.empty()) { + OSP_LOG_FATAL << "You must either invoke with -g to generate credentials, " + "or provide both a private key path and root certificate " + "using -p and -d"; + return 1; + } + + const char* interface_name = argv[optind]; + OSP_CHECK(interface_name && strlen(interface_name) > 0) + << "No interface name provided."; + + std::string device_id = + absl::StrCat("Standalone Receiver on ", interface_name); + ErrorOr<GeneratedCredentials> creds = GenerateCredentials( + device_id, private_key_path, developer_certificate_path); + OSP_CHECK(creds.is_value()) << creds.error(); + + const InterfaceInfo interface = GetInterfaceInfoFromName(interface_name); + OSP_CHECK(interface.GetIpAddressV4() || interface.GetIpAddressV6()); + if (std::all_of(interface.hardware_address.begin(), + interface.hardware_address.end(), + [](int e) { return e == 0; })) { + OSP_LOG_WARN + << "Hardware address is empty. Either you are on a loopback device " + "or getting the network interface information failed somehow. " + "Discovery publishing will be disabled."; + discovery_enabled = false; + } + + auto* const task_runner = new TaskRunnerImpl(&Clock::now); + PlatformClientPosix::Create(milliseconds(50), + std::unique_ptr<TaskRunnerImpl>(task_runner)); + RunCastService(task_runner, interface, std::move(creds.value()), + friendly_name, model_name, discovery_enabled); + PlatformClientPosix::ShutDown(); + + return 0; +} + +} // namespace +} // namespace cast +} // namespace openscreen + +int main(int argc, char* argv[]) { + return openscreen::cast::RunStandaloneReceiver(argc, argv); +} diff --git a/cast/standalone_receiver/mirroring_application.cc b/cast/standalone_receiver/mirroring_application.cc new file mode 100644 index 00000000..a04c401a --- /dev/null +++ b/cast/standalone_receiver/mirroring_application.cc @@ -0,0 +1,90 @@ +// 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_receiver/mirroring_application.h" + +#include "cast/common/public/message_port.h" +#include "cast/streaming/environment.h" +#include "cast/streaming/message_fields.h" +#include "cast/streaming/receiver_session.h" +#include "platform/api/task_runner.h" +#include "util/osp_logging.h" + +namespace openscreen { +namespace cast { + +const char kMirroringAppId[] = "0F5096E8"; +const char kMirroringAudioOnlyAppId[] = "85CDB22F"; + +const char kMirroringDisplayName[] = "Chrome Mirroring"; +const char kRemotingRpcNamespace[] = "urn:x-cast:com.google.cast.remoting"; + +MirroringApplication::MirroringApplication(TaskRunner* task_runner, + const IPAddress& interface_address, + ApplicationAgent* agent) + : task_runner_(task_runner), + interface_address_(interface_address), + app_ids_({kMirroringAppId, kMirroringAudioOnlyAppId}), + agent_(agent) { + OSP_DCHECK(task_runner_); + OSP_DCHECK(agent_); + agent_->RegisterApplication(this); +} + +MirroringApplication::~MirroringApplication() { + agent_->UnregisterApplication(this); // ApplicationAgent may call Stop(). + OSP_DCHECK(!current_session_); +} + +const std::vector<std::string>& MirroringApplication::GetAppIds() const { + return app_ids_; +} + +bool MirroringApplication::Launch(const std::string& app_id, + const Json::Value& app_params, + MessagePort* message_port) { + if ((app_id != kMirroringAppId && app_id != kMirroringAudioOnlyAppId) || + !message_port || current_session_) { + return false; + } + + wake_lock_ = ScopedWakeLock::Create(task_runner_); + environment_ = std::make_unique<Environment>( + &Clock::now, task_runner_, + IPEndpoint{interface_address_, kDefaultCastStreamingPort}); + controller_ = + std::make_unique<StreamingPlaybackController>(task_runner_, this); + current_session_ = std::make_unique<ReceiverSession>( + controller_.get(), environment_.get(), message_port, + ReceiverSession::Preferences{}); + return true; +} + +std::string MirroringApplication::GetSessionId() { + return current_session_ ? current_session_->session_id() : std::string(); +} + +std::string MirroringApplication::GetDisplayName() { + return current_session_ ? kMirroringDisplayName : std::string(); +} + +std::vector<std::string> MirroringApplication::GetSupportedNamespaces() { + return {kCastWebrtcNamespace, kRemotingRpcNamespace}; +} + +void MirroringApplication::Stop() { + current_session_.reset(); + controller_.reset(); + environment_.reset(); + wake_lock_.reset(); +} + +void MirroringApplication::OnPlaybackError(StreamingPlaybackController*, + Error error) { + OSP_LOG_ERROR << "[MirroringApplication] " << error; + agent_->StopApplicationIfRunning(this); // ApplicationAgent calls Stop(). +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/mirroring_application.h b/cast/standalone_receiver/mirroring_application.h new file mode 100644 index 00000000..c2e8ccc8 --- /dev/null +++ b/cast/standalone_receiver/mirroring_application.h @@ -0,0 +1,69 @@ +// 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. + +#ifndef CAST_STANDALONE_RECEIVER_MIRRORING_APPLICATION_H_ +#define CAST_STANDALONE_RECEIVER_MIRRORING_APPLICATION_H_ + +#include <memory> +#include <string> +#include <vector> + +#include "cast/receiver/application_agent.h" +#include "cast/standalone_receiver/streaming_playback_controller.h" +#include "platform/api/scoped_wake_lock.h" +#include "platform/api/serial_delete_ptr.h" +#include "platform/base/error.h" +#include "platform/base/ip_address.h" + +namespace openscreen { + +class TaskRunner; + +namespace cast { + +class MessagePort; +class ReceiverSession; + +// Implements a basic Cast V2 Mirroring Application which, at launch time, +// bootstraps a ReceiverSession and StreamingPlaybackController, which set-up +// and manage the media data streaming and play it out in an on-screen window. +class MirroringApplication final : public ApplicationAgent::Application, + public StreamingPlaybackController::Client { + public: + MirroringApplication(TaskRunner* task_runner, + const IPAddress& interface_address, + ApplicationAgent* agent); + + ~MirroringApplication() final; + + // ApplicationAgent::Application overrides. + const std::vector<std::string>& GetAppIds() const final; + bool Launch(const std::string& app_id, + const Json::Value& app_params, + MessagePort* message_port) final; + std::string GetSessionId() final; + std::string GetDisplayName() final; + std::vector<std::string> GetSupportedNamespaces() final; + void Stop() final; + + // StreamingPlaybackController::Client overrides + void OnPlaybackError(StreamingPlaybackController* controller, + Error error) final; + + private: + TaskRunner* const task_runner_; + const IPAddress interface_address_; + const std::vector<std::string> app_ids_; + ApplicationAgent* const agent_; + + SerialDeletePtr<ScopedWakeLock> wake_lock_; + std::unique_ptr<Environment> environment_; + std::unique_ptr<StreamingPlaybackController> controller_; + std::unique_ptr<ReceiverSession> current_session_; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_MIRRORING_APPLICATION_H_ diff --git a/cast/standalone_receiver/sdl_audio_player.cc b/cast/standalone_receiver/sdl_audio_player.cc new file mode 100644 index 00000000..c460a7ed --- /dev/null +++ b/cast/standalone_receiver/sdl_audio_player.cc @@ -0,0 +1,234 @@ +// Copyright 2019 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_receiver/sdl_audio_player.h" + +#include <chrono> +#include <sstream> +#include <utility> + +#include "absl/types/span.h" +#include "cast/standalone_receiver/avcodec_glue.h" +#include "util/big_endian.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/trace_logging.h" + +namespace openscreen { +namespace cast { + +namespace { + +constexpr char kAudioMediaType[] = "audio"; +constexpr SDL_AudioFormat kSDLAudioFormatUnknown = 0; + +bool SDLAudioSpecsAreDifferent(const SDL_AudioSpec& a, const SDL_AudioSpec& b) { + return a.freq != b.freq || a.format != b.format || a.channels != b.channels || + a.samples != b.samples; +} + +// Convert |num_channels| separate |planes| of audio, each containing +// |num_samples| samples, into a single array of |interleaved| samples. The +// memory backing all of the input arrays and the output array is assumed to be +// suitably aligned. +template <typename Element> +void InterleaveAudioSamples(const uint8_t* const planes[], + int num_channels, + int num_samples, + uint8_t* interleaved) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + // Note: This could be optimized with SIMD intrinsics for much better + // performance. + auto* dest = reinterpret_cast<Element*>(interleaved); + for (int ch = 0; ch < num_channels; ++ch) { + auto* const src = reinterpret_cast<const Element*>(planes[ch]); + for (int i = 0; i < num_samples; ++i) { + dest[i * num_channels] = src[i]; + } + ++dest; + } +} + +} // namespace + +SDLAudioPlayer::SDLAudioPlayer(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + Receiver* receiver, + AudioCodec codec, + std::function<void()> error_callback) + : SDLPlayerBase(now_function, + task_runner, + receiver, + CodecToString(codec), + std::move(error_callback), + kAudioMediaType) {} + +SDLAudioPlayer::~SDLAudioPlayer() { + if (device_ > 0) { + SDL_CloseAudioDevice(device_); + } +} + +ErrorOr<Clock::time_point> SDLAudioPlayer::RenderNextFrame( + const SDLPlayerBase::PresentableFrame& next_frame) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + OSP_DCHECK(next_frame.decoded_frame); + const AVFrame& frame = *next_frame.decoded_frame; + + pending_audio_spec_ = device_spec_; + pending_audio_spec_.freq = frame.sample_rate; + + // Punt if the AVFrame's format is not compatible with those supported by SDL. + const auto frame_format = static_cast<AVSampleFormat>(frame.format); + pending_audio_spec_.format = GetSDLAudioFormat(frame_format); + if (pending_audio_spec_.format == kSDLAudioFormatUnknown) { + std::ostringstream error; + error << "SDL does not support AVSampleFormat " << frame_format; + return Error(Error::Code::kUnknownError, error.str()); + } + + // Punt if the number of channels is not supported by SDL. + constexpr int kSdlSupportedChannelCounts[] = {1, 2, 4, 6}; + if (std::find(std::begin(kSdlSupportedChannelCounts), + std::end(kSdlSupportedChannelCounts), + frame.channels) == std::end(kSdlSupportedChannelCounts)) { + std::ostringstream error; + error << "SDL does not support " << frame.channels << " audio channels."; + return Error(Error::Code::kUnknownError, error.str()); + } + pending_audio_spec_.channels = frame.channels; + + // If |device_spec_| is different from what is required, re-compute the sample + // buffer size and the amount of time that represents. The |device_spec_| will + // be updated to match |pending_audio_spec_| later, in Present(). + if (SDLAudioSpecsAreDifferent(device_spec_, pending_audio_spec_)) { + // Find the smallest power-of-two number of samples that represents at least + // 20ms of audio. + constexpr auto kMinBufferDuration = milliseconds(20); + constexpr auto kOneSecond = seconds(1); + const auto required_samples = static_cast<int>( + pending_audio_spec_.freq * kMinBufferDuration / kOneSecond); + OSP_DCHECK_GE(required_samples, 1); + pending_audio_spec_.samples = 1 << av_log2(required_samples); + if (pending_audio_spec_.samples < required_samples) { + pending_audio_spec_.samples *= 2; + } + + approximate_lead_time_ = + (pending_audio_spec_.samples * Clock::to_duration(kOneSecond)) / + pending_audio_spec_.freq; + } + + // If the decoded audio is in planar format, interleave it for SDL. + const int bytes_per_sample = av_get_bytes_per_sample(frame_format); + const int byte_count = frame.nb_samples * frame.channels * bytes_per_sample; + if (av_sample_fmt_is_planar(frame_format)) { + interleaved_audio_buffer_.resize(byte_count); + switch (bytes_per_sample) { + case 1: + InterleaveAudioSamples<uint8_t>(frame.data, frame.channels, + frame.nb_samples, + &interleaved_audio_buffer_[0]); + break; + case 2: + InterleaveAudioSamples<uint16_t>(frame.data, frame.channels, + frame.nb_samples, + &interleaved_audio_buffer_[0]); + break; + case 4: + InterleaveAudioSamples<uint32_t>(frame.data, frame.channels, + frame.nb_samples, + &interleaved_audio_buffer_[0]); + break; + default: + OSP_NOTREACHED(); + break; + } + pending_audio_ = absl::Span<const uint8_t>(interleaved_audio_buffer_); + } else { + if (!interleaved_audio_buffer_.empty()) { + interleaved_audio_buffer_.clear(); + interleaved_audio_buffer_.shrink_to_fit(); + } + pending_audio_ = absl::Span<const uint8_t>(frame.data[0], byte_count); + } + + // SDL provides no way to query the actual lead time before audio samples will + // be output by the sound hardware. The only advice seems to be a quick + // comment about "the intent is double buffered audio." Thus, schedule the + // "push" of this data to happen such that the audio will be playing out of + // the hardware at the intended moment in time. + return next_frame.presentation_time - approximate_lead_time_; +} + +bool SDLAudioPlayer::RenderWhileIdle(const PresentableFrame* frame) { + // Do nothing. The SDL audio buffer will underrun and result in silence. + return false; +} + +void SDLAudioPlayer::Present() { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + if (state() != kScheduledToPresent) { + // In all other states, just do nothing. The SDL audio buffer will underrun + // and result in silence. + return; + } + + // Re-open audio device, if the audio format has changed. + if (SDLAudioSpecsAreDifferent(pending_audio_spec_, device_spec_)) { + if (device_ > 0) { + SDL_CloseAudioDevice(device_); + device_spec_ = SDL_AudioSpec{}; + } + + device_ = SDL_OpenAudioDevice(nullptr, // Pick default device. + 0, // For playback, not recording. + &pending_audio_spec_, // Desired format. + &device_spec_, // [output] Obtained format. + 0 // Disallow formats other than desired. + ); + if (device_ <= 0) { + std::ostringstream error; + error << "SDL_OpenAudioDevice failed: " << SDL_GetError(); + OnFatalError(error.str()); + return; + } + OSP_DCHECK(!SDLAudioSpecsAreDifferent(pending_audio_spec_, device_spec_)); + + constexpr int kSdlResumePlaybackCommand = 0; + SDL_PauseAudioDevice(device_, kSdlResumePlaybackCommand); + } + + SDL_QueueAudio(device_, pending_audio_.data(), pending_audio_.size()); +} + +// static +SDL_AudioFormat SDLAudioPlayer::GetSDLAudioFormat(AVSampleFormat format) { + switch (format) { + case AV_SAMPLE_FMT_U8P: + case AV_SAMPLE_FMT_U8: + return AUDIO_U8; + + case AV_SAMPLE_FMT_S16P: + case AV_SAMPLE_FMT_S16: + return IsBigEndianArchitecture() ? AUDIO_S16MSB : AUDIO_S16LSB; + + case AV_SAMPLE_FMT_S32P: + case AV_SAMPLE_FMT_S32: + return IsBigEndianArchitecture() ? AUDIO_S32MSB : AUDIO_S32LSB; + + case AV_SAMPLE_FMT_FLTP: + case AV_SAMPLE_FMT_FLT: + return IsBigEndianArchitecture() ? AUDIO_F32MSB : AUDIO_F32LSB; + + default: + // Either NONE, or the 64-bit formats are unsupported. + break; + } + + return kSDLAudioFormatUnknown; +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/sdl_audio_player.h b/cast/standalone_receiver/sdl_audio_player.h new file mode 100644 index 00000000..b21ce2cc --- /dev/null +++ b/cast/standalone_receiver/sdl_audio_player.h @@ -0,0 +1,64 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_SDL_AUDIO_PLAYER_H_ +#define CAST_STANDALONE_RECEIVER_SDL_AUDIO_PLAYER_H_ + +#include <string> +#include <vector> + +#include "cast/standalone_receiver/sdl_player_base.h" + +namespace openscreen { +namespace cast { + +// Consumes frames from a Receiver, decodes them, and renders them to an +// internally-owned SDL audio device. +class SDLAudioPlayer final : public SDLPlayerBase { + public: + // |error_callback| is run only if a fatal error occurs, at which point the + // player has halted and set |error_status()|. + SDLAudioPlayer(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + Receiver* receiver, + AudioCodec codec, + std::function<void()> error_callback); + + ~SDLAudioPlayer() final; + + private: + // SDLPlayerBase implementation. + ErrorOr<Clock::time_point> RenderNextFrame( + const SDLPlayerBase::PresentableFrame& frame) final; + bool RenderWhileIdle(const SDLPlayerBase::PresentableFrame* frame) final; + void Present() final; + + // Maps an AVSampleFormat enum to the SDL_AudioFormat equivalent. + static SDL_AudioFormat GetSDLAudioFormat(AVSampleFormat format); + + // The audio format determined by the last call to RenderCurrentFrame(). + SDL_AudioSpec pending_audio_spec_{}; + + // The amount of time before a target presentation time to call Present(), to + // account for audio buffering (the latency until samples reach the hardware). + Clock::duration approximate_lead_time_{}; + + // When the decoder provides planar data, this buffer is used for storing the + // interleaved conversion. + std::vector<uint8_t> interleaved_audio_buffer_; + + // Points to the memory containing the next chunk of interleaved audio. + absl::Span<const uint8_t> pending_audio_; + + // The currently-open SDL audio device (or zero, if not open). + SDL_AudioDeviceID device_ = 0; + + // The audio format being used by the currently-open SDL audio device. + SDL_AudioSpec device_spec_{}; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_SDL_AUDIO_PLAYER_H_ diff --git a/cast/standalone_receiver/sdl_glue.cc b/cast/standalone_receiver/sdl_glue.cc new file mode 100644 index 00000000..7c2c94da --- /dev/null +++ b/cast/standalone_receiver/sdl_glue.cc @@ -0,0 +1,42 @@ +// Copyright 2019 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_receiver/sdl_glue.h" + +#include "platform/api/task_runner.h" +#include "platform/api/time.h" +#include "util/osp_logging.h" + +namespace openscreen { +namespace cast { + +SDLEventLoopProcessor::SDLEventLoopProcessor( + TaskRunner* task_runner, + std::function<void()> quit_callback) + : alarm_(&Clock::now, task_runner), + quit_callback_(std::move(quit_callback)) { + alarm_.Schedule([this] { ProcessPendingEvents(); }, Alarm::kImmediately); +} + +SDLEventLoopProcessor::~SDLEventLoopProcessor() = default; + +void SDLEventLoopProcessor::ProcessPendingEvents() { + // Process all pending events. + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + OSP_VLOG << "SDL_QUIT received, invoking quit callback..."; + if (quit_callback_) { + quit_callback_(); + } + } + } + + // Schedule a task to come back and process more pending events. + constexpr auto kEventPollPeriod = std::chrono::milliseconds(10); + alarm_.ScheduleFromNow([this] { ProcessPendingEvents(); }, kEventPollPeriod); +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/sdl_glue.h b/cast/standalone_receiver/sdl_glue.h new file mode 100644 index 00000000..59a3a020 --- /dev/null +++ b/cast/standalone_receiver/sdl_glue.h @@ -0,0 +1,79 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_SDL_GLUE_H_ +#define CAST_STANDALONE_RECEIVER_SDL_GLUE_H_ + +#include <stdint.h> + +#include <functional> +#include <memory> + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" +#include <SDL2/SDL.h> +#pragma GCC diagnostic pop + +#include "util/alarm.h" + +namespace openscreen { + +class TaskRunner; + +namespace cast { + +template <uint32_t subsystem> +class ScopedSDLSubSystem { + public: + ScopedSDLSubSystem() { SDL_InitSubSystem(subsystem); } + ~ScopedSDLSubSystem() { SDL_QuitSubSystem(subsystem); } +}; + +// Macro that, for an SDL_Foo, generates code for: +// +// using SDLFooUniquePtr = std::unique_ptr<SDL_Foo, SDLFooDestroyer>; +// SDLFooUniquePtr MakeUniqueSDLFoo(...args...); +#define DEFINE_SDL_UNIQUE_PTR(name) \ + struct SDL##name##Destroyer { \ + void operator()(SDL_##name* obj) const { \ + if (obj) { \ + SDL_Destroy##name(obj); \ + } \ + } \ + }; \ + using SDL##name##UniquePtr = \ + std::unique_ptr<SDL_##name, SDL##name##Destroyer>; \ + template <typename... Args> \ + SDL##name##UniquePtr MakeUniqueSDL##name(Args&&... args) { \ + return SDL##name##UniquePtr( \ + SDL_Create##name(std::forward<Args>(args)...)); \ + } + +DEFINE_SDL_UNIQUE_PTR(Window); +DEFINE_SDL_UNIQUE_PTR(Renderer); +DEFINE_SDL_UNIQUE_PTR(Texture); + +#undef DEFINE_SDL_UNIQUE_PTR + +// A looping mechanism that runs the SDL event loop by scheduling periodic tasks +// to the given TaskRunner. Looping continues indefinitely, until the instance +// is destroyed. A client-provided quit callback is invoked whenever a SDL_QUIT +// event is received. +class SDLEventLoopProcessor { + public: + SDLEventLoopProcessor(TaskRunner* task_runner, + std::function<void()> quit_callback); + ~SDLEventLoopProcessor(); + + private: + void ProcessPendingEvents(); + + Alarm alarm_; + std::function<void()> quit_callback_; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_SDL_GLUE_H_ diff --git a/cast/standalone_receiver/sdl_player_base.cc b/cast/standalone_receiver/sdl_player_base.cc new file mode 100644 index 00000000..76ddb7bd --- /dev/null +++ b/cast/standalone_receiver/sdl_player_base.cc @@ -0,0 +1,256 @@ +// Copyright 2019 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_receiver/sdl_player_base.h" + +#include <chrono> +#include <sstream> +#include <utility> + +#include "absl/types/span.h" +#include "cast/standalone_receiver/avcodec_glue.h" +#include "cast/streaming/constants.h" +#include "cast/streaming/encoded_frame.h" +#include "util/big_endian.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/trace_logging.h" + +namespace openscreen { +namespace cast { + +SDLPlayerBase::SDLPlayerBase(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + Receiver* receiver, + const std::string& codec_name, + std::function<void()> error_callback, + const char* media_type) + : now_(now_function), + receiver_(receiver), + error_callback_(std::move(error_callback)), + media_type_(media_type), + decoder_(codec_name), + decode_alarm_(now_, task_runner), + render_alarm_(now_, task_runner), + presentation_alarm_(now_, task_runner) { + OSP_DCHECK(receiver_); + OSP_DCHECK(media_type_); + + decoder_.set_client(this); + receiver_->SetConsumer(this); + ResumeRendering(); +} + +SDLPlayerBase::~SDLPlayerBase() { + receiver_->SetConsumer(nullptr); + decoder_.set_client(nullptr); +} + +void SDLPlayerBase::OnFatalError(std::string message) { + state_ = kError; + error_status_ = Error(Error::Code::kUnknownError, std::move(message)); + + // Halt decoding and clear the rendering queue. + receiver_->SetConsumer(nullptr); + decoder_.set_client(nullptr); + decode_alarm_.Cancel(); + frames_to_render_.clear(); + + // Resume rendering, to emit an error indication (e.g., "red splash" screen). + ResumeRendering(); + + if (error_callback_) { + const auto callback = std::move(error_callback_); + callback(); + } +} + +Clock::time_point SDLPlayerBase::ResyncAndDeterminePresentationTime( + const EncodedFrame& frame) { + constexpr auto kMaxPlayoutDrift = milliseconds(100); + const auto media_time_since_last_sync = + (frame.rtp_timestamp - last_sync_rtp_timestamp_) + .ToDuration<Clock::duration>(receiver_->rtp_timebase()); + Clock::time_point presentation_time = + last_sync_reference_time_ + media_time_since_last_sync; + const auto drift = to_milliseconds(frame.reference_time - presentation_time); + if (drift > kMaxPlayoutDrift || drift < -kMaxPlayoutDrift) { + // Only log if not the very first frame. + OSP_LOG_IF(INFO, frame.frame_id != FrameId::first()) + << "Playout drift (" << drift.count() << " ms) exceeded threshold (" + << kMaxPlayoutDrift.count() << " ms) for " << media_type_ + << ". Re-synchronizing..."; + // This is the "big-stick" way to re-synchronize. If the amount of drift + // is small, a production-worthy player should "nudge" things gradually + // back into sync over several frames. + last_sync_rtp_timestamp_ = frame.rtp_timestamp; + last_sync_reference_time_ = frame.reference_time; + presentation_time = frame.reference_time; + } + return presentation_time; +} + +void SDLPlayerBase::OnFramesReady(int buffer_size) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + // Do not consume anything if there are too many frames in the pipeline + // already. + if (static_cast<int>(frames_to_render_.size()) > kMaxFramesInPipeline) { + return; + } + + // Consume the next frame. + const Clock::time_point start_time = now_(); + buffer_.Resize(buffer_size); + EncodedFrame frame = receiver_->ConsumeNextFrame(buffer_.GetSpan()); + + // Create the tracking state for the frame in the player pipeline. + OSP_DCHECK_EQ(frames_to_render_.count(frame.frame_id), 0); + PendingFrame& pending_frame = frames_to_render_[frame.frame_id]; + pending_frame.start_time = start_time; + + pending_frame.presentation_time = ResyncAndDeterminePresentationTime(frame); + + // Start decoding the frame. This call may synchronously call back into the + // AVCodecDecoder::Client methods in this class. + decoder_.Decode(frame.frame_id, buffer_); +} + +void SDLPlayerBase::OnFrameDecoded(FrameId frame_id, const AVFrame& frame) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + const auto it = frames_to_render_.find(frame_id); + if (it == frames_to_render_.end()) { + return; + } + OSP_DCHECK(!it->second.decoded_frame); + // av_clone_frame() does a shallow copy here, incrementing a ref-count on the + // memory backing the frame. + it->second.decoded_frame = AVFrameUniquePtr(av_frame_clone(&frame)); + ResumeRendering(); +} + +void SDLPlayerBase::OnDecodeError(FrameId frame_id, std::string message) { + const auto it = frames_to_render_.find(frame_id); + if (it != frames_to_render_.end()) { + frames_to_render_.erase(it); + } + OSP_LOG_WARN << "Requesting " << media_type_ + << " key frame because of error decoding" << frame_id << ": " + << message; + receiver_->RequestKeyFrame(); + ResumeDecoding(); +} + +void SDLPlayerBase::RenderAndSchedulePresentation() { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + // If something has already been scheduled to present at an exact time point, + // don't render anything new yet. + if (state_ == kScheduledToPresent) { + return; + } + + // If no frames are available, just re-render the currently-presented frame + // (or the error screen). + auto it = + (state_ == kError) ? frames_to_render_.end() : frames_to_render_.begin(); + if (it == frames_to_render_.end() || !it->second.decoded_frame) { + if (RenderWhileIdle(state_ == kPresented ? ¤t_frame_ : nullptr)) { + // Schedule presentation to happen after a rather lengthy interval, to + // minimize redraw/etc. resource usage while doing "idle mode" play-out. + // The interval here, is "lengthy" from the program's perspective, but + // reasonably "snappy" from the user's perspective. + constexpr auto kIdlePresentInterval = milliseconds(250); + presentation_alarm_.ScheduleFromNow( + [this] { + Present(); + ResumeRendering(); + }, + kIdlePresentInterval); + } + return; + } + + // Skip late frames, to render the first not-late frame. If all decoded frames + // are late, skip-forward to the least-late frame. + const Clock::time_point now = now_(); + while (it->second.presentation_time < now) { + const auto next_it = std::next(it); + if (next_it == frames_to_render_.end() || !next_it->second.decoded_frame) { + break; + } + frames_to_render_.erase(it); // Drop the late frame. + it = next_it; + } + + // Remove the frame from the queue, making it the |current_frame_|. Then, + // render it and, if successful, schedule its presentation. + current_frame_ = std::move(it->second); + frames_to_render_.erase(it); + const ErrorOr<Clock::time_point> presentation_time = + RenderNextFrame(current_frame_); + if (!presentation_time) { + OnFatalError(presentation_time.error().message()); + return; + } + state_ = kScheduledToPresent; + presentation_alarm_.Schedule( + [this] { + Present(); + if (state_ == kScheduledToPresent) { + state_ = kPresented; + } + ResumeRendering(); + }, + presentation_time.value()); + + // Resume consuming/decoding frames, since some of the prior OnFramesReady() + // calls may have been ignored to leave things in the Receiver's queue. + ResumeDecoding(); + + // Compute how long it took to decode/render this frame, and notify the + // Receiver of the recent-average per-frame processing time. This is used by + // the Receiver to determine when to drop late frames. + const Clock::duration measured_processing_time = + now_() - current_frame_.start_time; + constexpr int kCumulativeAveragePoints = 8; + recent_processing_time_ = + ((kCumulativeAveragePoints - 1) * recent_processing_time_ + + 1 * measured_processing_time) / + kCumulativeAveragePoints; + receiver_->SetPlayerProcessingTime(recent_processing_time_); +} + +void SDLPlayerBase::ResumeDecoding() { + decode_alarm_.Schedule( + [this] { + const int buffer_size = receiver_->AdvanceToNextFrame(); + if (buffer_size != Receiver::kNoFramesReady) { + OnFramesReady(buffer_size); + } + }, + Alarm::kImmediately); +} + +void SDLPlayerBase::ResumeRendering() { + render_alarm_.Schedule([this] { RenderAndSchedulePresentation(); }, + Alarm::kImmediately); +} + +// static +constexpr int SDLPlayerBase::kMaxFramesInPipeline; + +SDLPlayerBase::PresentableFrame::PresentableFrame() = default; +SDLPlayerBase::PresentableFrame::~PresentableFrame() = default; +SDLPlayerBase::PresentableFrame::PresentableFrame(PresentableFrame&&) noexcept = + default; +SDLPlayerBase::PresentableFrame& SDLPlayerBase::PresentableFrame::operator=( + PresentableFrame&&) noexcept = default; + +SDLPlayerBase::PendingFrame::PendingFrame() = default; +SDLPlayerBase::PendingFrame::~PendingFrame() = default; +SDLPlayerBase::PendingFrame::PendingFrame(PendingFrame&&) noexcept = default; +SDLPlayerBase::PendingFrame& SDLPlayerBase::PendingFrame::operator=( + PendingFrame&&) noexcept = default; + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/sdl_player_base.h b/cast/standalone_receiver/sdl_player_base.h new file mode 100644 index 00000000..4e268e8a --- /dev/null +++ b/cast/standalone_receiver/sdl_player_base.h @@ -0,0 +1,181 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_SDL_PLAYER_BASE_H_ +#define CAST_STANDALONE_RECEIVER_SDL_PLAYER_BASE_H_ + +#include <stdint.h> + +#include <functional> +#include <map> +#include <string> + +#include "cast/standalone_receiver/decoder.h" +#include "cast/standalone_receiver/sdl_glue.h" +#include "cast/streaming/message_fields.h" +#include "cast/streaming/receiver.h" +#include "platform/api/task_runner.h" +#include "platform/api/time.h" +#include "platform/base/error.h" + +namespace openscreen { +namespace cast { + +// Common base class that consumes frames from a Receiver, decodes them, and +// plays them out via the appropriate SDL subsystem. Subclasses implement the +// specifics, based on the type of media (audio or video). +class SDLPlayerBase : public Receiver::Consumer, public Decoder::Client { + public: + ~SDLPlayerBase() override; + + // Returns OK unless a fatal error has occurred. + const Error& error_status() const { return error_status_; } + + protected: + // Current player state, which is used to determine what to render/present, + // and how frequently. + enum PlayerState { + kWaitingForFirstFrame, // Render silent "blue splash" screen at idle FPS. + kScheduledToPresent, // Present new content at an exact time point. + kPresented, // Continue presenting same content at idle FPS. + kError, // Render silent "red splash" screen at idle FPS. + }; + + // A decoded frame and its target presentation time. + struct PresentableFrame { + Clock::time_point presentation_time; + AVFrameUniquePtr decoded_frame; + + PresentableFrame(); + ~PresentableFrame(); + PresentableFrame(PresentableFrame&& other) noexcept; + PresentableFrame& operator=(PresentableFrame&& other) noexcept; + }; + + // |error_callback| is run only if a fatal error occurs, at which point the + // player has halted and set |error_status()|. |media_type| should be "audio" + // or "video" (only used when logging). + SDLPlayerBase(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + Receiver* receiver, + const std::string& codec_name, + std::function<void()> error_callback, + const char* media_type); + + PlayerState state() const { return state_; } + + // Called back from either |decoder_| or a player subclass to handle a fatal + // error event. + void OnFatalError(std::string message) final; + + // Renders the |frame| and returns its [possibly adjusted] presentation time. + virtual ErrorOr<Clock::time_point> RenderNextFrame( + const PresentableFrame& frame) = 0; + + // Called to render when the player has no new content, and returns true if a + // Present() is necessary. |frame| may be null, if it is not available. This + // method can be called before the first frame, after any frame, or after a + // fatal error has occurred. + virtual bool RenderWhileIdle(const PresentableFrame* frame) = 0; + + // Presents the rendering from the last call to RenderNextFrame() or + // RenderWhileIdle(). + virtual void Present() = 0; + + private: + struct PendingFrame : public PresentableFrame { + Clock::time_point start_time; + + PendingFrame(); + ~PendingFrame(); + PendingFrame(PendingFrame&& other) noexcept; + PendingFrame& operator=(PendingFrame&& other) noexcept; + }; + + // Receiver::Consumer implementation. + void OnFramesReady(int next_frame_buffer_size) final; + + // Determine the presentation time of the frame. Ideally, this will occur + // based on the time progression of the media, given by the RTP timestamps. + // However, if this falls too far out-of-sync with the system reference clock, + // re-synchronize, possibly causing user-visible "jank." + Clock::time_point ResyncAndDeterminePresentationTime( + const EncodedFrame& frame); + + // AVCodecDecoder::Client implementation. These are called-back from + // |decoder_| to provide results. + void OnFrameDecoded(FrameId frame_id, const AVFrame& frame) final; + void OnDecodeError(FrameId frame_id, std::string message) final; + + // Calls RenderNextFrame() on the next available decoded frame, and schedules + // its presentation. If no decoded frame is available, RenderWhileIdle() is + // called instead. + void RenderAndSchedulePresentation(); + + // Schedules an explicit check to see if more frames are ready for + // consumption. Normally, the Receiver will notify this Consumer when more + // frames are ready. However, there are cases where prior notifications were + // ignored because there were too many frames in the player's pipeline. Thus, + // whenever frames are removed from the pipeline, this method should be + // called. + void ResumeDecoding(); + + // Called whenever a frame has been decoded, presentation of a prior frame has + // completed, and/or the player has encountered a state change that might + // require rendering/presenting a different output. + void ResumeRendering(); + + const ClockNowFunctionPtr now_; + Receiver* const receiver_; + std::function<void()> error_callback_; // Run once by OnFatalError(). + const char* const media_type_; // For logging only. + + // Set to the error code that placed the player in a fatal error state. + Error error_status_; + + // Current player state, which is used to determine what to render/present, + // and how frequently. + PlayerState state_ = kWaitingForFirstFrame; + + // Queue of frames currently being decoded and decoded frames awaiting + // rendering. + + std::map<FrameId, PendingFrame> frames_to_render_; + + // Buffer for holding EncodedFrame::data. + Decoder::Buffer buffer_; + + // Associates a RTP timestamp with a local clock time point. This is updated + // whenever the media (RTP) timestamps drift too much away from the rate at + // which the local clock ticks. This is important for A/V synchronization. + RtpTimeTicks last_sync_rtp_timestamp_{}; + Clock::time_point last_sync_reference_time_{}; + + Decoder decoder_; + + // The decoded frame to be rendered/presented. + PendingFrame current_frame_; + + // A cumulative moving average of recent single-frame processing times + // (consume + decode + render). This is passed to the Cast Receiver so that it + // can determine when to drop late frames. + Clock::duration recent_processing_time_{}; + + // Alarms that execute the various stages of the player pipeline at certain + // times. + Alarm decode_alarm_; + Alarm render_alarm_; + Alarm presentation_alarm_; + + // Maximum number of frames in the decode/render pipeline. This limit is about + // making sure the player uses resources efficiently: It is better for frames + // to remain in the Receiver's queue until this player is ready to process + // them. + static constexpr int kMaxFramesInPipeline = 8; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_SDL_PLAYER_BASE_H_ diff --git a/cast/standalone_receiver/sdl_video_player.cc b/cast/standalone_receiver/sdl_video_player.cc new file mode 100644 index 00000000..999545de --- /dev/null +++ b/cast/standalone_receiver/sdl_video_player.cc @@ -0,0 +1,206 @@ +// Copyright 2019 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_receiver/sdl_video_player.h" + +#include <sstream> +#include <utility> + +#include "cast/standalone_receiver/avcodec_glue.h" +#include "util/osp_logging.h" +#include "util/trace_logging.h" + +namespace openscreen { +namespace cast { + +namespace { +constexpr char kVideoMediaType[] = "video"; +} // namespace + +SDLVideoPlayer::SDLVideoPlayer(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + Receiver* receiver, + VideoCodec codec, + SDL_Renderer* renderer, + std::function<void()> error_callback) + : SDLPlayerBase(now_function, + task_runner, + receiver, + CodecToString(codec), + std::move(error_callback), + kVideoMediaType), + renderer_(renderer) { + OSP_DCHECK(renderer_); +} + +SDLVideoPlayer::~SDLVideoPlayer() = default; + +bool SDLVideoPlayer::RenderWhileIdle( + const SDLPlayerBase::PresentableFrame* frame) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + // Attempt to re-render the same content. + if (state() == kPresented && frame) { + const auto result = RenderNextFrame(*frame); + if (result) { + return true; + } + OnFatalError(result.error().message()); + // Fall-through to the "red splash" rendering below. + } + + if (state() == kError) { + // Paint "red splash" to indicate an error state. + constexpr struct { int r = 128, g = 0, b = 0, a = 255; } kRedSplashColor; + SDL_SetRenderDrawColor(renderer_, kRedSplashColor.r, kRedSplashColor.g, + kRedSplashColor.b, kRedSplashColor.a); + SDL_RenderClear(renderer_); + } else if (state() == kWaitingForFirstFrame || !frame) { + // Paint "blue splash" to indicate the "waiting for first frame" state. + constexpr struct { int r = 0, g = 0, b = 128, a = 255; } kBlueSplashColor; + SDL_SetRenderDrawColor(renderer_, kBlueSplashColor.r, kBlueSplashColor.g, + kBlueSplashColor.b, kBlueSplashColor.a); + SDL_RenderClear(renderer_); + } + + return state() != kScheduledToPresent; +} + +ErrorOr<Clock::time_point> SDLVideoPlayer::RenderNextFrame( + const SDLPlayerBase::PresentableFrame& frame) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + OSP_DCHECK(frame.decoded_frame); + const AVFrame& picture = *frame.decoded_frame; + + // Punt if the |picture| format is not compatible with those supported by SDL. + const uint32_t sdl_format = GetSDLPixelFormat(picture); + if (sdl_format == SDL_PIXELFORMAT_UNKNOWN) { + std::ostringstream error; + error << "SDL does not support AVPixelFormat " << picture.format; + return Error(Error::Code::kUnknownError, error.str()); + } + + // If there is already a SDL texture, check that its format and size matches + // that of |picture|. If not, release the existing texture. + if (texture_) { + uint32_t texture_format = SDL_PIXELFORMAT_UNKNOWN; + int texture_width = -1; + int texture_height = -1; + SDL_QueryTexture(texture_.get(), &texture_format, nullptr, &texture_width, + &texture_height); + if (texture_format != sdl_format || texture_width != picture.width || + texture_height != picture.height) { + texture_.reset(); + } + } + + // If necessary, recreate a SDL texture having the same format and size as + // that of |picture|. + if (!texture_) { + const auto EvalDescriptionString = [&] { + std::ostringstream error; + error << SDL_GetPixelFormatName(sdl_format) << " at " << picture.width + << "×" << picture.height; + return error.str(); + }; + OSP_LOG_INFO << "Creating SDL texture for " << EvalDescriptionString(); + texture_ = + MakeUniqueSDLTexture(renderer_, sdl_format, SDL_TEXTUREACCESS_STREAMING, + picture.width, picture.height); + if (!texture_) { + std::ostringstream error; + error << "Unable to (re)create SDL texture for format: " + << EvalDescriptionString(); + return Error(Error::Code::kUnknownError, error.str()); + } + } + + // Upload the |picture_| to the SDL texture. + void* pixels = nullptr; + int stride = 0; + SDL_LockTexture(texture_.get(), nullptr, &pixels, &stride); + const auto picture_format = static_cast<AVPixelFormat>(picture.format); + const int pixels_size = av_image_get_buffer_size( + picture_format, picture.width, picture.height, stride); + constexpr int kSDLTextureRowAlignment = 1; // SDL doesn't use word-alignment. + av_image_copy_to_buffer(static_cast<uint8_t*>(pixels), pixels_size, + picture.data, picture.linesize, picture_format, + picture.width, picture.height, + kSDLTextureRowAlignment); + SDL_UnlockTexture(texture_.get()); + + // Render the SDL texture to the render target. Quality-related issues that a + // production-worthy player should account for that are not being done here: + // + // 1. Need to account for AVPicture's sample_aspect_ratio property. Otherwise, + // content may appear "squashed" in one direction to the user. + // + // 2. SDL has no concept of color space, and so the color information provided + // with the AVPicture might not match the assumptions being made within + // SDL. Content may appear with washed-out colors, not-entirely-black + // blacks, striped gradients, etc. + const SDL_Rect src_rect = { + static_cast<int>(picture.crop_left), static_cast<int>(picture.crop_top), + picture.width - static_cast<int>(picture.crop_left + picture.crop_right), + picture.height - + static_cast<int>(picture.crop_top + picture.crop_bottom)}; + SDL_Rect dst_rect = {0, 0, 0, 0}; + SDL_RenderGetLogicalSize(renderer_, &dst_rect.w, &dst_rect.h); + if (src_rect.w != dst_rect.w || src_rect.h != dst_rect.h) { + // Make the SDL rendering size the same as the frame's visible size. This + // lets SDL automatically handle letterboxing and scaling details, so that + // the video fits within the on-screen window. + dst_rect.w = src_rect.w; + dst_rect.h = src_rect.h; + SDL_RenderSetLogicalSize(renderer_, dst_rect.w, dst_rect.h); + } + // Clear with black, for the "letterboxing" borders. + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); + SDL_RenderClear(renderer_); + SDL_RenderCopy(renderer_, texture_.get(), &src_rect, &dst_rect); + + return frame.presentation_time; +} + +void SDLVideoPlayer::Present() { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); + SDL_RenderPresent(renderer_); +} + +// static +uint32_t SDLVideoPlayer::GetSDLPixelFormat(const AVFrame& picture) { + switch (picture.format) { + case AV_PIX_FMT_NONE: + break; + case AV_PIX_FMT_YUV420P: + return SDL_PIXELFORMAT_IYUV; + case AV_PIX_FMT_YUYV422: + return SDL_PIXELFORMAT_YUY2; + case AV_PIX_FMT_UYVY422: + return SDL_PIXELFORMAT_UYVY; + case AV_PIX_FMT_YVYU422: + return SDL_PIXELFORMAT_YVYU; + case AV_PIX_FMT_NV12: + return SDL_PIXELFORMAT_NV12; + case AV_PIX_FMT_NV21: + return SDL_PIXELFORMAT_NV21; + case AV_PIX_FMT_RGB24: + return SDL_PIXELFORMAT_RGB24; + case AV_PIX_FMT_BGR24: + return SDL_PIXELFORMAT_BGR24; + case AV_PIX_FMT_ARGB: + return SDL_PIXELFORMAT_ARGB32; + case AV_PIX_FMT_RGBA: + return SDL_PIXELFORMAT_RGBA32; + case AV_PIX_FMT_ABGR: + return SDL_PIXELFORMAT_ABGR32; + case AV_PIX_FMT_BGRA: + return SDL_PIXELFORMAT_BGRA32; + default: + break; + } + return SDL_PIXELFORMAT_UNKNOWN; +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/sdl_video_player.h b/cast/standalone_receiver/sdl_video_player.h new file mode 100644 index 00000000..609c860c --- /dev/null +++ b/cast/standalone_receiver/sdl_video_player.h @@ -0,0 +1,60 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_SDL_VIDEO_PLAYER_H_ +#define CAST_STANDALONE_RECEIVER_SDL_VIDEO_PLAYER_H_ + +#include <string> + +#include "cast/standalone_receiver/sdl_player_base.h" +#include "cast/streaming/constants.h" + +namespace openscreen { +namespace cast { + +// Consumes frames from a Receiver, decodes them, and renders them to a +// SDL_Renderer. +class SDLVideoPlayer final : public SDLPlayerBase { + public: + // |error_callback| is run only if a fatal error occurs, at which point the + // player has halted and set |error_status()|. + SDLVideoPlayer(ClockNowFunctionPtr now_function, + TaskRunner* task_runner, + Receiver* receiver, + VideoCodec codec_name, + SDL_Renderer* renderer, + std::function<void()> error_callback); + + ~SDLVideoPlayer() final; + + private: + // Renders the "blue splash" (if waiting) or "red splash" (on error), or + // otherwise re-renders |frame|; scheduling presentation at an "idle FPS" + // rate. + bool RenderWhileIdle(const SDLPlayerBase::PresentableFrame* frame) final; + + // Uploads the decoded picture in |frame| to a SDL texture and draws it using + // the SDL |renderer_|. + ErrorOr<Clock::time_point> RenderNextFrame( + const SDLPlayerBase::PresentableFrame& frame) final; + + // Makes whatever is currently drawn to the SDL |renderer_| be presented + // on-screen. + void Present() final; + + // Maps an AVFrame format enum to the SDL equivalent. + static uint32_t GetSDLPixelFormat(const AVFrame& picture); + + // The SDL renderer drawn to. + SDL_Renderer* const renderer_; + + // The SDL texture to which the current frame's image is uploaded for + // accelerated 2D rendering. + SDLTextureUniquePtr texture_; +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_SDL_VIDEO_PLAYER_H_ diff --git a/cast/standalone_receiver/streaming_playback_controller.cc b/cast/standalone_receiver/streaming_playback_controller.cc new file mode 100644 index 00000000..f9196ae5 --- /dev/null +++ b/cast/standalone_receiver/streaming_playback_controller.cc @@ -0,0 +1,99 @@ +// Copyright 2019 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_receiver/streaming_playback_controller.h" + +#include <string> + +#if defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) +#include "cast/standalone_receiver/sdl_audio_player.h" +#include "cast/standalone_receiver/sdl_glue.h" +#include "cast/standalone_receiver/sdl_video_player.h" +#else +#include "cast/standalone_receiver/dummy_player.h" +#endif // defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) + +#include "util/trace_logging.h" + +namespace openscreen { +namespace cast { + +#if defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) +StreamingPlaybackController::StreamingPlaybackController( + TaskRunner* task_runner, + StreamingPlaybackController::Client* client) + : task_runner_(task_runner), + client_(client), + sdl_event_loop_(task_runner_, [this] { + client_->OnPlaybackError(this, + Error{Error::Code::kOperationCancelled, + std::string("SDL event loop closed.")}); + }) { + OSP_DCHECK(task_runner_ != nullptr); + OSP_DCHECK(client_ != nullptr); + constexpr int kDefaultWindowWidth = 1280; + constexpr int kDefaultWindowHeight = 720; + window_ = MakeUniqueSDLWindow( + "Cast Streaming Receiver Demo", + SDL_WINDOWPOS_UNDEFINED /* initial X position */, + SDL_WINDOWPOS_UNDEFINED /* initial Y position */, kDefaultWindowWidth, + kDefaultWindowHeight, SDL_WINDOW_RESIZABLE); + OSP_CHECK(window_) << "Failed to create SDL window: " << SDL_GetError(); + renderer_ = MakeUniqueSDLRenderer(window_.get(), -1, 0); + OSP_CHECK(renderer_) << "Failed to create SDL renderer: " << SDL_GetError(); +} +#else +StreamingPlaybackController::StreamingPlaybackController( + TaskRunner* task_runner, + StreamingPlaybackController::Client* client) + : task_runner_(task_runner), client_(client) { + OSP_DCHECK(task_runner_ != nullptr); + OSP_DCHECK(client_ != nullptr); +} +#endif // defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) + +void StreamingPlaybackController::OnMirroringNegotiated( + const ReceiverSession* session, + ReceiverSession::ConfiguredReceivers receivers) { + TRACE_DEFAULT_SCOPED(TraceCategory::kStandaloneReceiver); +#if defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) + if (receivers.audio_receiver) { + audio_player_ = std::make_unique<SDLAudioPlayer>( + &Clock::now, task_runner_, receivers.audio_receiver, + receivers.audio_config.codec, [this] { + client_->OnPlaybackError(this, audio_player_->error_status()); + }); + } + if (receivers.video_receiver) { + video_player_ = std::make_unique<SDLVideoPlayer>( + &Clock::now, task_runner_, receivers.video_receiver, + receivers.video_config.codec, renderer_.get(), [this] { + client_->OnPlaybackError(this, video_player_->error_status()); + }); + } +#else + if (receivers.audio_receiver) { + audio_player_ = std::make_unique<DummyPlayer>(receivers.audio_receiver); + } + + if (receivers.video_receiver) { + video_player_ = std::make_unique<DummyPlayer>(receivers.video_receiver); + } +#endif // defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) +} + +void StreamingPlaybackController::OnReceiversDestroying( + const ReceiverSession* session, + ReceiversDestroyingReason reason) { + audio_player_.reset(); + video_player_.reset(); +} + +void StreamingPlaybackController::OnError(const ReceiverSession* session, + Error error) { + client_->OnPlaybackError(this, error); +} + +} // namespace cast +} // namespace openscreen diff --git a/cast/standalone_receiver/streaming_playback_controller.h b/cast/standalone_receiver/streaming_playback_controller.h new file mode 100644 index 00000000..1e81ed5b --- /dev/null +++ b/cast/standalone_receiver/streaming_playback_controller.h @@ -0,0 +1,70 @@ +// Copyright 2019 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. + +#ifndef CAST_STANDALONE_RECEIVER_STREAMING_PLAYBACK_CONTROLLER_H_ +#define CAST_STANDALONE_RECEIVER_STREAMING_PLAYBACK_CONTROLLER_H_ + +#include <memory> + +#include "cast/streaming/receiver_session.h" +#include "platform/impl/task_runner.h" + +#if defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) +#include "cast/standalone_receiver/sdl_audio_player.h" +#include "cast/standalone_receiver/sdl_glue.h" +#include "cast/standalone_receiver/sdl_video_player.h" +#else +#include "cast/standalone_receiver/dummy_player.h" +#endif // defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) + +namespace openscreen { +namespace cast { + +class StreamingPlaybackController final : public ReceiverSession::Client { + public: + class Client { + public: + virtual void OnPlaybackError(StreamingPlaybackController* controller, + Error error) = 0; + }; + + StreamingPlaybackController(TaskRunner* task_runner, + StreamingPlaybackController::Client* client); + + // ReceiverSession::Client overrides. + void OnMirroringNegotiated( + const ReceiverSession* session, + ReceiverSession::ConfiguredReceivers receivers) override; + + void OnReceiversDestroying(const ReceiverSession* session, + ReceiversDestroyingReason reason) override; + + void OnError(const ReceiverSession* session, Error error) override; + + private: + TaskRunner* const task_runner_; + StreamingPlaybackController::Client* client_; + +#if defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) + // NOTE: member ordering is important, since the sub systems must be + // first-constructed, last-destroyed. Make sure any new SDL related + // members are added below the sub systems. + const ScopedSDLSubSystem<SDL_INIT_AUDIO> sdl_audio_sub_system_; + const ScopedSDLSubSystem<SDL_INIT_VIDEO> sdl_video_sub_system_; + const SDLEventLoopProcessor sdl_event_loop_; + + SDLWindowUniquePtr window_; + SDLRendererUniquePtr renderer_; + std::unique_ptr<SDLAudioPlayer> audio_player_; + std::unique_ptr<SDLVideoPlayer> video_player_; +#else + std::unique_ptr<DummyPlayer> audio_player_; + std::unique_ptr<DummyPlayer> video_player_; +#endif // defined(CAST_STANDALONE_RECEIVER_HAVE_EXTERNAL_LIBS) +}; + +} // namespace cast +} // namespace openscreen + +#endif // CAST_STANDALONE_RECEIVER_STREAMING_PLAYBACK_CONTROLLER_H_ |