aboutsummaryrefslogtreecommitdiff
path: root/cast/standalone_receiver
diff options
context:
space:
mode:
Diffstat (limited to 'cast/standalone_receiver')
-rw-r--r--cast/standalone_receiver/BUILD.gn79
-rw-r--r--cast/standalone_receiver/DEPS10
-rw-r--r--cast/standalone_receiver/avcodec_glue.h56
-rw-r--r--cast/standalone_receiver/cast_service.cc109
-rw-r--r--cast/standalone_receiver/cast_service.h77
-rw-r--r--cast/standalone_receiver/decoder.cc221
-rw-r--r--cast/standalone_receiver/decoder.h99
-rw-r--r--cast/standalone_receiver/dummy_player.cc44
-rw-r--r--cast/standalone_receiver/dummy_player.h40
-rwxr-xr-xcast/standalone_receiver/install_demo_deps_debian.sh7
-rwxr-xr-xcast/standalone_receiver/install_demo_deps_raspian.sh8
-rw-r--r--cast/standalone_receiver/main.cc248
-rw-r--r--cast/standalone_receiver/mirroring_application.cc90
-rw-r--r--cast/standalone_receiver/mirroring_application.h69
-rw-r--r--cast/standalone_receiver/sdl_audio_player.cc234
-rw-r--r--cast/standalone_receiver/sdl_audio_player.h64
-rw-r--r--cast/standalone_receiver/sdl_glue.cc42
-rw-r--r--cast/standalone_receiver/sdl_glue.h79
-rw-r--r--cast/standalone_receiver/sdl_player_base.cc256
-rw-r--r--cast/standalone_receiver/sdl_player_base.h181
-rw-r--r--cast/standalone_receiver/sdl_video_player.cc206
-rw-r--r--cast/standalone_receiver/sdl_video_player.h60
-rw-r--r--cast/standalone_receiver/streaming_playback_controller.cc99
-rw-r--r--cast/standalone_receiver/streaming_playback_controller.h70
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 ? &current_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_