| // 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 |