From 47df3b231a8a5f6a41f154ac01d8eee8e73e4dd7 Mon Sep 17 00:00:00 2001 From: Cameron Gutman Date: Sun, 19 Jan 2020 19:46:45 -0800 Subject: [PATCH] Add support for HEVC and client-specified colorspaces --- assets/sunshine.conf | 7 ++++- sunshine/config.cpp | 6 ++-- sunshine/config.h | 2 +- sunshine/nvhttp.cpp | 18 ++++++++---- sunshine/stream.cpp | 33 ++++++++++++++++----- sunshine/video.cpp | 70 ++++++++++++++++++++++++++++++++++++++++++-- sunshine/video.h | 3 ++ 7 files changed, 119 insertions(+), 20 deletions(-) diff --git a/assets/sunshine.conf b/assets/sunshine.conf index 88740b9c..e98b50f3 100644 --- a/assets/sunshine.conf +++ b/assets/sunshine.conf @@ -86,8 +86,13 @@ qp = 28 # Number of threads used by ffmpeg to encode the video threads = 8 +# Allows the client to request HEVC Main or HEVC Main10 video streams. +# HEVC is more CPU-intensive to encode, so enabling this may reduce performance. +# If set to 0 (default), Sunshine will not advertise support for HEVC +# If set to 1, Sunshine will advertise support for HEVC Main profile +# If set to 2, Sunshine will advertise support for HEVC Main and Main10 (HDR) profiles +# hevc_mode = 2 # See x264 --fullhelp for the different presets -profile = baseline preset = superfast tune = zerolatency diff --git a/sunshine/config.cpp b/sunshine/config.cpp index d242dedb..6280f941 100644 --- a/sunshine/config.cpp +++ b/sunshine/config.cpp @@ -21,7 +21,7 @@ video_t video { 4, // threads - "baseline"s, // profile + 0, // hevc_mode "superfast"s, // preset "zerolatency"s // tune }; @@ -158,7 +158,9 @@ void parse_file(const char *file) { int_f(vars, "crf", video.crf); int_f(vars, "qp", video.qp); int_f(vars, "threads", video.threads); - string_f(vars, "profile", video.profile); + int_between_f(vars, "hevc_mode", video.hevc_mode, { + 0, 2 + }); string_f(vars, "preset", video.preset); string_f(vars, "tune", video.tune); diff --git a/sunshine/config.h b/sunshine/config.h index f8540ed8..df950b3b 100644 --- a/sunshine/config.h +++ b/sunshine/config.h @@ -12,7 +12,7 @@ struct video_t { int threads; // Number threads used by ffmpeg - std::string profile; + int hevc_mode; std::string preset; std::string tune; }; diff --git a/sunshine/nvhttp.cpp b/sunshine/nvhttp.cpp index dff5ba49..eb02d8de 100644 --- a/sunshine/nvhttp.cpp +++ b/sunshine/nvhttp.cpp @@ -437,13 +437,21 @@ void serverinfo(std::shared_ptr::Response> res tree.put("root.appversion", VERSION); tree.put("root.GfeVersion", GFE_VERSION); tree.put("root.uniqueid", config::nvhttp.unique_id); - tree.put("root.mac", "42:45:F0:65:D6:F4"); + tree.put("root.mac", "00:00:00:00:00:00"); + tree.put("root.MaxLumaPixelsHEVC", config::video.hevc_mode > 0 ? "1869449984" : "0"); tree.put("root.LocalIP", local_ip); - if(config::nvhttp.external_ip.empty()) { - tree.put("root.ExternalIP", local_ip); + if(config::video.hevc_mode == 2) { + tree.put("root.ServerCodecModeSupport", "3843"); + } + else if(config::video.hevc_mode == 1) { + tree.put("root.ServerCodecModeSupport", "259"); } else { + tree.put("root.ServerCodecModeSupport", "3"); + } + + if(!config::nvhttp.external_ip.empty()) { tree.put("root.ExternalIP", config::nvhttp.external_ip); } @@ -485,7 +493,7 @@ void applist(resp_https_t response, req_https_t request) { pt::ptree desktop; apps.put(".status_code", 200); - desktop.put("IsHdrSupported"s, 0); + desktop.put("IsHdrSupported"s, config::video.hevc_mode == 2 ? 1 : 0); desktop.put("AppTitle"s, "Desktop"); desktop.put("ID"s, 1); @@ -493,7 +501,7 @@ void applist(resp_https_t response, req_https_t request) { for(auto &proc : proc::proc.get_apps()) { pt::ptree app; - app.put("IsHdrSupported"s, 0); + app.put("IsHdrSupported"s, config::video.hevc_mode == 2 ? 1 : 0); app.put("AppTitle"s, proc.name); app.put("ID"s, x++); diff --git a/sunshine/stream.cpp b/sunshine/stream.cpp index 6fa65aaf..1113f621 100644 --- a/sunshine/stream.cpp +++ b/sunshine/stream.cpp @@ -703,13 +703,21 @@ void videoThread(video::idr_event_t idr_events) { // make sure moonlight recognizes the nalu code for IDR frames if (packet->flags & AV_PKT_FLAG_KEY) { - //TODO: Not all encoders encode their IDR frames with `"\000\000\001e"` - auto seq_i_frame_old = "\000\000\001e"sv; - auto seq_i_frame = "\000\000\000\001e"sv; - - assert(std::search(std::begin(payload), std::end(payload), std::begin(seq_i_frame), std::end(seq_i_frame)) == - std::end(payload)); - payload_new = replace(payload, seq_i_frame_old, seq_i_frame); + // TODO: Not all encoders encode their IDR frames with the 4 byte NALU prefix + if(config.monitor.videoFormat == 0) { + auto h264_i_frame_old = "\000\000\001e"sv; + auto h264_i_frame = "\000\000\000\001e"sv; + assert(std::search(std::begin(payload), std::end(payload), std::begin(h264_i_frame), std::end(h264_i_frame)) == + std::end(payload)); + payload_new = replace(payload, h264_i_frame_old, h264_i_frame); + } + else { + auto hevc_i_frame_old = "\000\000\001("sv; + auto hevc_i_frame = "\000\000\000\001("sv; + assert(std::search(std::begin(payload), std::end(payload), std::begin(hevc_i_frame), std::end(hevc_i_frame)) == + std::end(payload)); + payload_new = replace(payload, hevc_i_frame_old, hevc_i_frame); + } payload = {(char *) payload_new.data(), payload_new.size()}; } @@ -858,7 +866,7 @@ void cmd_describe(host_t &host, peer_t peer, msg_t&& req) { option.content = const_cast(seqn_str.c_str()); // FIXME: Moonlight will accept the payload, but the value of the option is not correct - respond(host, peer, &option, 200, "OK", req->sequenceNumber, "surround-params=NONE"sv); + respond(host, peer, &option, 200, "OK", req->sequenceNumber, "sprop-parameter-sets=AAAAAU;surround-params=NONE"sv); } void cmd_setup(host_t &host, peer_t peer, msg_t &&req) { @@ -958,6 +966,12 @@ void cmd_announce(host_t &host, peer_t peer, msg_t &&req) { } } + // Initialize any omitted parameters to defaults + args.try_emplace("x-nv-video[0].encoderCscMode"sv, "0"sv); + args.try_emplace("x-nv-vqos[0].bitStreamFormat"sv, "0"sv); + args.try_emplace("x-nv-video[0].dynamicRangeMode"sv, "0"sv); + args.try_emplace("x-nv-aqos.packetDuration"sv, "5"sv); + try { auto &config = session.config; @@ -973,6 +987,9 @@ void cmd_announce(host_t &host, peer_t peer, msg_t &&req) { config.monitor.bitrate = util::from_view(args.at("x-nv-vqos[0].bw.maximumBitrateKbps"sv)); config.monitor.slicesPerFrame = util::from_view(args.at("x-nv-video[0].videoEncoderSlicesPerFrame"sv)); config.monitor.numRefFrames = util::from_view(args.at("x-nv-video[0].maxNumReferenceFrames"sv)); + config.monitor.encoderCscMode = util::from_view(args.at("x-nv-video[0].encoderCscMode"sv)); + config.monitor.videoFormat = util::from_view(args.at("x-nv-vqos[0].bitStreamFormat"sv)); + config.monitor.dynamicRange = util::from_view(args.at("x-nv-video[0].dynamicRangeMode"sv)); } catch(std::out_of_range &) { diff --git a/sunshine/video.cpp b/sunshine/video.cpp index 56926d1f..d8cd84aa 100644 --- a/sunshine/video.cpp +++ b/sunshine/video.cpp @@ -90,7 +90,14 @@ void encodeThread( config_t config) { int framerate = config.framerate; - auto codec = avcodec_find_encoder(AV_CODEC_ID_H264); + AVCodec *codec; + + if(config.videoFormat == 0) { + codec = avcodec_find_encoder(AV_CODEC_ID_H264); + } + else { + codec = avcodec_find_encoder(AV_CODEC_ID_HEVC); + } ctx_t ctx{avcodec_alloc_context3(codec)}; @@ -100,7 +107,53 @@ void encodeThread( ctx->height = config.height; ctx->time_base = AVRational{1, framerate}; ctx->framerate = AVRational{framerate, 1}; - ctx->pix_fmt = AV_PIX_FMT_YUV420P; + + if(config.videoFormat == 0) { + ctx->profile = FF_PROFILE_H264_HIGH; + } + else if(config.dynamicRange == 0) { + ctx->profile = FF_PROFILE_HEVC_MAIN; + } + else { + ctx->profile = FF_PROFILE_HEVC_MAIN_10; + } + + if(config.dynamicRange == 0) { + ctx->pix_fmt = AV_PIX_FMT_YUV420P; + } + else { + ctx->pix_fmt = AV_PIX_FMT_YUV420P10; + } + + ctx->color_range = (config.encoderCscMode & 0x1) ? AVCOL_RANGE_JPEG : AVCOL_RANGE_MPEG; + + int swsColorSpace; + switch (config.encoderCscMode >> 1) { + case 0: + default: + // Rec. 601 + ctx->color_primaries = AVCOL_PRI_SMPTE170M; + ctx->color_trc = AVCOL_TRC_SMPTE170M; + ctx->colorspace = AVCOL_SPC_SMPTE170M; + swsColorSpace = SWS_CS_SMPTE170M; + break; + + case 1: + // Rec. 709 + ctx->color_primaries = AVCOL_PRI_BT709; + ctx->color_trc = AVCOL_TRC_BT709; + ctx->colorspace = AVCOL_SPC_BT709; + swsColorSpace = SWS_CS_ITU709; + break; + + case 2: + // Rec. 2020 + ctx->color_primaries = AVCOL_PRI_BT2020; + ctx->color_trc = AVCOL_TRC_BT2020_10; + ctx->colorspace = AVCOL_SPC_BT2020_NCL; + swsColorSpace = SWS_CS_BT2020; + break; + } // B-frames delay decoder output, so never use them ctx->max_b_frames = 0; @@ -118,7 +171,6 @@ void encodeThread( AVDictionary *options {nullptr}; - av_dict_set(&options, "profile", config::video.profile.c_str(), 0); av_dict_set(&options, "preset", config::video.preset.c_str(), 0); av_dict_set(&options, "tune", config::video.tune.c_str(), 0); @@ -135,6 +187,14 @@ void encodeThread( else { av_dict_set_int(&options, "qp", config::video.qp, 0); } + + if(config.videoFormat == 1) { + // x265's Info SEI is so long that it causes the IDR picture data to be + // kicked to the 2nd packet in the frame, breaking Moonlight's parsing logic. + // It also looks like gop_size isn't passed on to x265, so we have to set + // 'keyint=-1' in the parameters ourselves. + av_dict_set(&options, "x265-params", "info=0:keyint=-1", 0); + } ctx->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); ctx->flags2 |= AV_CODEC_FLAG2_FAST; @@ -169,6 +229,10 @@ void encodeThread( ctx->width, ctx->height, ctx->pix_fmt, SWS_LANCZOS | SWS_ACCURATE_RND, nullptr, nullptr, nullptr)); + + sws_setColorspaceDetails(sws.get(), sws_getCoefficients(SWS_CS_DEFAULT), 0, + sws_getCoefficients(swsColorSpace), config.encoderCscMode & 0x1, + 0, 1 << 16, 1 << 16); } if(idr_events->peek()) { diff --git a/sunshine/video.h b/sunshine/video.h index 3d0a25b0..16025370 100644 --- a/sunshine/video.h +++ b/sunshine/video.h @@ -22,6 +22,9 @@ struct config_t { int bitrate; int slicesPerFrame; int numRefFrames; + int encoderCscMode; + int videoFormat; + int dynamicRange; }; void capture_display(packet_queue_t packets, idr_event_t idr_events, config_t config);