diff --git a/plugins/win-dshow/CMakeLists.txt b/plugins/win-dshow/CMakeLists.txt index 2dc878f78..2d23c95ed 100644 --- a/plugins/win-dshow/CMakeLists.txt +++ b/plugins/win-dshow/CMakeLists.txt @@ -9,6 +9,7 @@ set(win-dshow_HEADERS set(win-dshow_SOURCES win-dshow.cpp + win-dshow-encoder.cpp dshow-plugin.cpp ffmpeg-decode.c) diff --git a/plugins/win-dshow/data/locale/en-US.ini b/plugins/win-dshow/data/locale/en-US.ini index e97cf7155..d2b3e7506 100644 --- a/plugins/win-dshow/data/locale/en-US.ini +++ b/plugins/win-dshow/data/locale/en-US.ini @@ -1,3 +1,4 @@ +# video capture device text VideoCaptureDevice="Video Capture Device" Device="Device" ConfigureAudio="Configure Audio" @@ -14,3 +15,6 @@ VideoFormat.Any="Any" VideoFormat.Unknown="Unknown (%1)" UseCustomAudioDevice="Use custom audio device" AudioDevice="Audio Device" + +# encoder text +Bitrate="Bitrate" diff --git a/plugins/win-dshow/dshow-plugin.cpp b/plugins/win-dshow/dshow-plugin.cpp index 0d19d53b2..a306ce0f4 100644 --- a/plugins/win-dshow/dshow-plugin.cpp +++ b/plugins/win-dshow/dshow-plugin.cpp @@ -4,9 +4,11 @@ OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("win-dshow", "en-US") extern void RegisterDShowSource(); +extern void RegisterDShowEncoders(); bool obs_module_load(void) { RegisterDShowSource(); + RegisterDShowEncoders(); return true; } diff --git a/plugins/win-dshow/win-dshow-encoder.cpp b/plugins/win-dshow/win-dshow-encoder.cpp new file mode 100644 index 000000000..554f4c9a0 --- /dev/null +++ b/plugins/win-dshow/win-dshow-encoder.cpp @@ -0,0 +1,361 @@ +#include +#include +#include +#include +#include "libdshowcapture/dshowcapture.hpp" + +using namespace DShow; +using namespace std; + +struct DShowEncoder { + obs_encoder_t *context; + VideoEncoder encoder; + + VideoEncoderConfig config; + + const wchar_t *device; + video_format format; + long long frameInterval; + + bool first = true; + DARRAY(uint8_t) firstPacket; + DARRAY(uint8_t) header; + + inline DShowEncoder(obs_encoder_t *context_, const wchar_t *device_) + : context(context_), + device(device_) + { + da_init(firstPacket); + da_init(header); + } + + inline ~DShowEncoder() + { + da_free(firstPacket); + da_free(header); + } + + inline void ParseFirstPacket(const uint8_t *data, size_t size); + + inline bool Update(obs_data_t *settings); + inline bool Encode(struct encoder_frame *frame, + struct encoder_packet *packet, bool *received_packet); +}; + +static const char *GetDShowEncoderName(void) +{ + return "DShow Encoder (temp)"; +} + +static inline void FindDevice(DeviceId &id, const wchar_t *name) +{ + vector devices; + DShow::VideoEncoder::EnumEncoders(devices); + + for (const DeviceId &device : devices) { + if (device.name.find(name) != string::npos) { + id = device; + break; + } + } +} + +/* + * As far as I can tell the devices only seem to support 1024x768 and 1280x720 + * resolutions, so I'm just going to cap it to those resolutions by aspect. + * + * XXX: This should really not be hard-coded. Problem is I don't know how to + * properly query the encoding capabilities of the devices. + */ +static const double standardAspect = 1024.0 / 768.0; +static const double wideAspect = 1280.0 / 720.0; + +inline bool DShowEncoder::Update(obs_data_t *settings) +{ + DStr deviceName; + DeviceId id; + + FindDevice(id, device); + + video_t *video = obs_encoder_video(context); + const struct video_output_info *voi = video_output_get_info(video); + + int bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + int width = (int)obs_encoder_get_width(context); + int height = (int)obs_encoder_get_height(context); + + double aspect = double(width) / double(height); + + if (keyint_sec == 0) + keyint_sec = 2; + if (fabs(aspect - standardAspect) < fabs(aspect - wideAspect)) { + width = 1024; + height = 768; + } else { + width = 1280; + height = 720; + } + + int keyint = keyint_sec * voi->fps_num / voi->fps_den; + + frameInterval = voi->fps_den * 10000000 / voi->fps_num; + + config.fpsNumerator = voi->fps_num; + config.fpsDenominator = voi->fps_den; + config.bitrate = bitrate; + config.keyframeInterval = keyint; + config.cx = width; + config.cy = height; + config.name = id.name; + config.path = id.path; + + first = true; + da_resize(firstPacket, 0); + da_resize(header, 0); + + dstr_from_wcs(deviceName, id.name.c_str()); + + DStr encoder_name; + dstr_from_wcs(encoder_name, config.name.c_str()); + blog(LOG_DEBUG, "win-dshow-encoder:\n" + "\tencoder: %s\n" + "\twidth: %d\n" + "\theight: %d\n" + "\tfps_num: %d\n" + "\tfps_den: %d", + deviceName->array, + (int)width, + (int)height, + (int)voi->fps_num, + (int)voi->fps_den); + + return encoder.SetConfig(config); +} + +static bool UpdateDShowEncoder(void *data, obs_data_t *settings) +{ + return reinterpret_cast(data)->Update(settings); +} + +static inline void *CreateDShowEncoder(obs_data_t *settings, + obs_encoder_t *context, const wchar_t *device) +{ + DShowEncoder *encoder = nullptr; + + try { + encoder = new DShowEncoder(context, device); + UpdateDShowEncoder(encoder, settings); + + } catch (const char *error) { + blog(LOG_ERROR, "Could not create DirectShow encoder '%s': %s", + obs_encoder_get_name(context), error); + } + + UNUSED_PARAMETER(settings); + return encoder; +} + +static void *CreateC985Encoder(obs_data_t *settings, obs_encoder_t *context) +{ + return CreateDShowEncoder(settings, context, L"C985"); +} + +static void *CreateC353Encoder(obs_data_t *settings, obs_encoder_t *context) +{ + return CreateDShowEncoder(settings, context, L"C353"); +} + +static void DestroyDShowEncoder(void *data) +{ + delete reinterpret_cast(data); +} + +/* the first packet contains the SPS/PPS (header) NALs, so parse the first + * packet and separate the NALs */ +inline void DShowEncoder::ParseFirstPacket(const uint8_t *data, size_t size) +{ + const uint8_t *nal_start, *nal_end, *nal_codestart; + const uint8_t *end = data + size; + int type; + + nal_start = obs_avc_find_startcode(data, end); + nal_end = nullptr; + while (nal_end != end) { + nal_codestart = nal_start; + + while (nal_start < end && !*(nal_start++)); + + if (nal_start == end) + break; + + type = nal_start[0] & 0x1F; + + nal_end = obs_avc_find_startcode(nal_start, end); + if (!nal_end) + nal_end = end; + + if (type == OBS_NAL_SPS || type == OBS_NAL_PPS) { + da_push_back_array(header, nal_codestart, + nal_end - nal_codestart); + + } else { + da_push_back_array(firstPacket, nal_codestart, + nal_end - nal_codestart); + } + + nal_start = nal_end; + } +} + +inline bool DShowEncoder::Encode(struct encoder_frame *frame, + struct encoder_packet *packet, bool *received_packet) +{ + unsigned char *frame_data[DSHOW_MAX_PLANES] = {}; + size_t frame_sizes[DSHOW_MAX_PLANES] = {}; + EncoderPacket dshowPacket; + bool new_packet = false; + + /* The encoders expect YV12, so swap the chroma planes for encoding */ + if (format == VIDEO_FORMAT_I420) { + frame_data[0] = frame->data[0]; + frame_data[1] = frame->data[2]; + frame_data[2] = frame->data[1]; + frame_sizes[0] = frame->linesize[0] * config.cy; + frame_sizes[1] = frame->linesize[2] * config.cy / 2; + frame_sizes[2] = frame->linesize[1] * config.cy / 2; + } + + long long actualPTS = frame->pts * frameInterval; + + bool success = encoder.Encode(frame_data, frame_sizes, + actualPTS, actualPTS + frameInterval, + dshowPacket, new_packet); + if (!success) + return false; + + if (new_packet && !!dshowPacket.data && !!dshowPacket.size) { + packet->data = dshowPacket.data; + packet->size = dshowPacket.size; + packet->type = OBS_ENCODER_VIDEO; + packet->pts = dshowPacket.pts / frameInterval; + packet->dts = dshowPacket.dts / frameInterval; + packet->keyframe = obs_avc_keyframe(packet->data, packet->size); + + /* first packet must be parsed in order to retrieve header */ + if (first) { + first = false; + ParseFirstPacket(packet->data, packet->size); + packet->data = firstPacket.array; + packet->size = firstPacket.num; + } + + *received_packet = true; + } + + return true; +} + +static bool DShowEncode(void *data, struct encoder_frame *frame, + struct encoder_packet *packet, bool *received_packet) +{ + return reinterpret_cast(data)->Encode(frame, packet, + received_packet); +} + +static bool GetDShowExtraData(void *data, uint8_t **extra_data, size_t *size) +{ + DShowEncoder *encoder = reinterpret_cast(data); + + *extra_data = encoder->header.array; + *size = encoder->header.num; + + return *size > 0; +} + +static inline bool ValidResolution(uint32_t width, uint32_t height) +{ + return (width == 1280 && height == 720) || + (width == 1024 && height == 768); +} + +static bool GetDShowVideoInfo(void *data, struct video_scale_info *info) +{ + DShowEncoder *encoder = reinterpret_cast(data); + video_t *video = obs_encoder_video(encoder->context); + const struct video_output_info *vid_info = video_output_get_info(video); + + encoder->format = VIDEO_FORMAT_I420; + + if (vid_info->format == VIDEO_FORMAT_I420 && + ValidResolution(vid_info->width, vid_info->height)) + return false; + + info->format = VIDEO_FORMAT_I420; + info->width = vid_info->width; + info->height = vid_info->height; + info->range = VIDEO_RANGE_DEFAULT; + info->colorspace = VIDEO_CS_DEFAULT; + + double aspect = double(info->width) / double(info->height); + + if (fabs(aspect - standardAspect) < fabs(aspect - wideAspect)) { + info->width = 1024; + info->height = 768; + } else { + info->width = 1280; + info->height = 720; + } + + return true; +} + +static void GetDShowEncoderDefauts(obs_data_t *settings) +{ + obs_data_set_default_int(settings, "bitrate", 1000); +} + +static obs_properties_t *GetDShowEncoderProperties(void *data) +{ + obs_properties_t *ppts = obs_properties_create(); + + obs_properties_add_int(ppts, "bitrate", obs_module_text("Bitrate"), + 1000, 60000, 1); + + UNUSED_PARAMETER(data); + return ppts; +} + +void RegisterDShowEncoders() +{ + obs_encoder_info info = {}; + info.type = OBS_ENCODER_VIDEO; + info.codec = "h264"; + info.get_name = GetDShowEncoderName; + info.destroy = DestroyDShowEncoder; + info.encode = DShowEncode; + info.update = UpdateDShowEncoder; + info.get_defaults = GetDShowEncoderDefauts; + info.get_properties = GetDShowEncoderProperties; + info.get_extra_data = GetDShowExtraData; + info.get_video_info = GetDShowVideoInfo; + + vector devices; + DShow::VideoEncoder::EnumEncoders(devices); + + bool foundC985 = false; + + for (const DeviceId &device : devices) { + if (!foundC985 && device.name.find(L"C985") != string::npos) { + info.id = "dshow_c985_h264"; + info.create = CreateC985Encoder; + obs_register_encoder(&info); + foundC985 = true; + + } else if (device.name.find(L"C353") != string::npos) { + info.id = "dshow_c353_h264"; + info.create = CreateC353Encoder; + obs_register_encoder(&info); + } + } +}