From 5e75e7822d65c10b1626a93a94566a30dfc4d871 Mon Sep 17 00:00:00 2001 From: iperov Date: Sun, 8 May 2022 14:57:15 +0400 Subject: [PATCH] StreamOutput: added mpegts udp output --- apps/DeepFaceLive/backend/StreamOutput.py | 94 ++++++++++++++---- apps/DeepFaceLive/ui/QStreamOutput.py | 13 ++- .../ui/widgets/QLineEditCSWText.py | 45 +++++++++ doc/setup_tutorial_windows/Media_source.png | Bin 0 -> 2760 bytes .../Media_source_config.png | Bin 0 -> 5328 bytes .../Media_source_stream_output.png | Bin 0 -> 2651 bytes .../setup_for_streaming.md | 23 ++++- xlib/streamer/FFMPEGStreamer.py | 52 ++++++++++ xlib/streamer/__init__.py | 1 + 9 files changed, 203 insertions(+), 25 deletions(-) create mode 100644 apps/DeepFaceLive/ui/widgets/QLineEditCSWText.py create mode 100644 doc/setup_tutorial_windows/Media_source.png create mode 100644 doc/setup_tutorial_windows/Media_source_config.png create mode 100644 doc/setup_tutorial_windows/Media_source_stream_output.png create mode 100644 xlib/streamer/FFMPEGStreamer.py create mode 100644 xlib/streamer/__init__.py diff --git a/apps/DeepFaceLive/backend/StreamOutput.py b/apps/DeepFaceLive/backend/StreamOutput.py index 611d308..ecd40af 100644 --- a/apps/DeepFaceLive/backend/StreamOutput.py +++ b/apps/DeepFaceLive/backend/StreamOutput.py @@ -10,6 +10,7 @@ from xlib import os as lib_os from xlib import time as lib_time from xlib.image import ImageProcessor from xlib.mp import csw as lib_csw +from xlib.streamer import FFMPEGStreamer from .BackendBase import (BackendConnection, BackendDB, BackendHost, BackendSignal, BackendWeakHeap, BackendWorker, @@ -43,16 +44,17 @@ class SourceType(IntEnum): SOURCE_N_MERGED_FRAME = 5 SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME = 6 -ViewModeNames = ['@StreamOutput.SourceType.SOURCE_FRAME', - '@StreamOutput.SourceType.ALIGNED_FACE', +ViewModeNames = ['@StreamOutput.SourceType.SOURCE_FRAME', + '@StreamOutput.SourceType.ALIGNED_FACE', '@StreamOutput.SourceType.SWAPPED_FACE', - '@StreamOutput.SourceType.MERGED_FRAME', - '@StreamOutput.SourceType.MERGED_FRAME_OR_SOURCE_FRAME', + '@StreamOutput.SourceType.MERGED_FRAME', + '@StreamOutput.SourceType.MERGED_FRAME_OR_SOURCE_FRAME', '@StreamOutput.SourceType.SOURCE_N_MERGED_FRAME', '@StreamOutput.SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME', ] + class StreamOutputWorker(BackendWorker): def get_state(self) -> 'WorkerState': return super().get_state() def get_control_sheet(self) -> 'Sheet.Worker': return super().get_control_sheet() @@ -73,7 +75,9 @@ class StreamOutputWorker(BackendWorker): self._wnd_name = 'DeepFaceLive output' self._wnd_showing = False - + + self._streamer = FFMPEGStreamer() + lib_os.set_timer_resolution(1) state, cs = self.get_state(), self.get_control_sheet() @@ -84,7 +88,10 @@ class StreamOutputWorker(BackendWorker): cs.target_delay.call_on_number(self.on_cs_target_delay) cs.save_sequence_path.call_on_paths(self.on_cs_save_sequence_path) cs.save_fill_frame_gap.call_on_flag(self.on_cs_save_fill_frame_gap) - + cs.is_streaming.call_on_flag(self.on_cs_is_streaming) + cs.stream_addr.call_on_text(self.on_cs_stream_addr) + cs.stream_port.call_on_number(self.on_cs_stream_port) + cs.source_type.enable() cs.source_type.set_choices(SourceType, ViewModeNames, none_choice_name='@misc.menu_select') cs.source_type.select(state.source_type) @@ -107,16 +114,26 @@ class StreamOutputWorker(BackendWorker): state.is_showing_window = not state.is_showing_window cs.show_hide_window.signal() - cs.save_sequence_path.enable() cs.save_sequence_path.set_config( lib_csw.Paths.Config.Directory('Choose output sequence directory', directory_path=save_default_path) ) cs.save_sequence_path.set_paths(state.sequence_path) cs.save_fill_frame_gap.enable() cs.save_fill_frame_gap.set_flag(state.save_fill_frame_gap if state.save_fill_frame_gap is not None else True ) + + cs.is_streaming.enable() + cs.is_streaming.set_flag(state.is_streaming if state.is_streaming is not None else False ) + + cs.stream_addr.enable() + cs.stream_addr.set_text(state.stream_addr if state.stream_addr is not None else '127.0.0.1') + + cs.stream_port.enable() + cs.stream_port.set_config(lib_csw.Number.Config(min=1, max=9999, decimals=0, allow_instant_update=True)) + cs.stream_port.set_number(state.stream_port if state.stream_port is not None else 1234) - - + def on_stop(self): + self._streamer.stop() + def on_cs_source_type(self, idx, source_type): state, cs = self.get_state(), self.get_control_sheet() if source_type == SourceType.ALIGNED_FACE: @@ -126,10 +143,10 @@ class StreamOutputWorker(BackendWorker): else: cs.aligned_face_id.disable() state.source_type = source_type - + self.save_state() self.reemit_frame_signal.send() - + def show_window(self): state, cs = self.get_state(), self.get_control_sheet() cv2.namedWindow(self._wnd_name) @@ -189,6 +206,23 @@ class StreamOutputWorker(BackendWorker): state.save_fill_frame_gap = save_fill_frame_gap self.save_state() + def on_cs_is_streaming(self, is_streaming): + state, cs = self.get_state(), self.get_control_sheet() + state.is_streaming = is_streaming + self.save_state() + + def on_cs_stream_addr(self, stream_addr): + state, cs = self.get_state(), self.get_control_sheet() + state.stream_addr = stream_addr + self.save_state() + self._streamer.set_addr_port(state.stream_addr, state.stream_port) + + def on_cs_stream_port(self, stream_port): + state, cs = self.get_state(), self.get_control_sheet() + state.stream_port = stream_port + self.save_state() + self._streamer.set_addr_port(state.stream_addr, state.stream_port) + def on_tick(self): cs, state = self.get_control_sheet(), self.get_state() @@ -204,7 +238,9 @@ class StreamOutputWorker(BackendWorker): source_type = state.source_type if source_type is not None and \ - (state.is_showing_window or state.sequence_path is not None): + (state.is_showing_window or \ + state.sequence_path is not None or \ + state.is_streaming): buffered_frames = self.buffered_frames view_image = None @@ -213,7 +249,7 @@ class StreamOutputWorker(BackendWorker): view_image = bcd.get_image(bcd.get_frame_image_name()) elif source_type in [SourceType.MERGED_FRAME, SourceType.MERGED_FRAME_OR_SOURCE_FRAME]: view_image = bcd.get_image(bcd.get_merged_image_name()) - if view_image is None and source_type == SourceType.MERGED_FRAME_OR_SOURCE_FRAME: + if view_image is None and source_type == SourceType.MERGED_FRAME_OR_SOURCE_FRAME: view_image = bcd.get_image(bcd.get_frame_image_name()) elif source_type == SourceType.ALIGNED_FACE: @@ -228,17 +264,17 @@ class StreamOutputWorker(BackendWorker): view_image = bcd.get_image(fsi.face_swap_image_name) if view_image is not None: break - + elif source_type in [SourceType.SOURCE_N_MERGED_FRAME, SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME]: source_frame = bcd.get_image(bcd.get_frame_image_name()) if source_frame is not None: source_frame = ImageProcessor(source_frame).to_ufloat32().get_image('HWC') - + merged_frame = bcd.get_image(bcd.get_merged_image_name()) - - if merged_frame is None and source_type == SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME: + + if merged_frame is None and source_type == SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME: merged_frame = source_frame - + if source_frame is not None and merged_frame is not None: view_image = np.concatenate( (source_frame, merged_frame), 1 ) @@ -259,8 +295,13 @@ class StreamOutputWorker(BackendWorker): pr = buffered_frames.process() img = pr.new_data - if state.is_showing_window and img is not None: - cv2.imshow(self._wnd_name, img) + if img is not None: + if state.is_streaming: + img = ImageProcessor(view_image).to_uint8().get_image('HWC') + self._streamer.push_frame(img) + + if state.is_showing_window: + cv2.imshow(self._wnd_name, img) if state.is_showing_window: cv2.waitKey(1) @@ -277,7 +318,10 @@ class Sheet: self.save_sequence_path = lib_csw.Paths.Client() self.save_sequence_path_error = lib_csw.Error.Client() self.save_fill_frame_gap = lib_csw.Flag.Client() - + self.is_streaming = lib_csw.Flag.Client() + self.stream_addr = lib_csw.Text.Client() + self.stream_port = lib_csw.Number.Client() + class Worker(lib_csw.Sheet.Worker): def __init__(self): super().__init__() @@ -289,7 +333,10 @@ class Sheet: self.save_sequence_path = lib_csw.Paths.Host() self.save_sequence_path_error = lib_csw.Error.Host() self.save_fill_frame_gap = lib_csw.Flag.Host() - + self.is_streaming = lib_csw.Flag.Host() + self.stream_addr = lib_csw.Text.Host() + self.stream_port = lib_csw.Number.Host() + class WorkerState(BackendWorkerState): source_type : SourceType = None is_showing_window : bool = None @@ -297,3 +344,6 @@ class WorkerState(BackendWorkerState): target_delay : int = None sequence_path : Path = None save_fill_frame_gap : bool = None + is_streaming : bool = None + stream_addr : str = None + stream_port : int = None diff --git a/apps/DeepFaceLive/ui/QStreamOutput.py b/apps/DeepFaceLive/ui/QStreamOutput.py index e512cf4..b43c476 100644 --- a/apps/DeepFaceLive/ui/QStreamOutput.py +++ b/apps/DeepFaceLive/ui/QStreamOutput.py @@ -1,5 +1,7 @@ from localization import L +from resources.fonts import QXFontDB from xlib import qt as qtx +from xlib.qt.widgets.QXLabel import QXLabel from ..backend import StreamOutput from .widgets.QBackendPanel import QBackendPanel @@ -9,6 +11,7 @@ from .widgets.QComboBoxCSWDynamicSingleSwitch import \ from .widgets.QErrorCSWError import QErrorCSWError from .widgets.QLabelCSWNumber import QLabelCSWNumber from .widgets.QLabelPopupInfo import QLabelPopupInfo +from .widgets.QLineEditCSWText import QLineEditCSWText from .widgets.QPathEditCSWPaths import QPathEditCSWPaths from .widgets.QSpinBoxCSWNumber import QSpinBoxCSWNumber from .widgets.QXPushButtonCSWSignal import QXPushButtonCSWSignal @@ -39,6 +42,12 @@ class QStreamOutput(QBackendPanel): q_save_fill_frame_gap_label = QLabelPopupInfo(label=L('@QStreamOutput.save_fill_frame_gap'), popup_info_text=L('@QStreamOutput.help.save_fill_frame_gap')) q_save_fill_frame_gap = QCheckBoxCSWFlag(cs.save_fill_frame_gap, reflect_state_widgets=[q_save_fill_frame_gap_label]) + q_is_streaming_label = QLabelPopupInfo(label='mpegts udp://') + q_is_streaming = QCheckBoxCSWFlag(cs.is_streaming, reflect_state_widgets=[q_is_streaming_label]) + + q_stream_addr = QLineEditCSWText(cs.stream_addr, font=QXFontDB.get_fixedwidth_font()) + q_stream_port = QSpinBoxCSWNumber(cs.stream_port) + grid_l = qtx.QXGridLayout(spacing=5) row = 0 grid_l.addWidget(q_average_fps_label, row, 0, 1, 1, alignment=qtx.AlignRight | qtx.AlignVCenter ) @@ -61,9 +70,11 @@ class QStreamOutput(QBackendPanel): row += 1 grid_l.addLayout( qtx.QXHBoxLayout([q_save_fill_frame_gap, 4, q_save_fill_frame_gap_label]), row, 1, 1, 2, alignment=qtx.AlignLeft | qtx.AlignVCenter ) row += 1 - grid_l.addWidget(q_save_sequence_path_error, row, 0, 1, 3) row += 1 + grid_l.addLayout( qtx.QXHBoxLayout([q_is_streaming, 4, q_is_streaming_label]), row, 0, 1, 1, alignment=qtx.AlignRight | qtx.AlignVCenter ) + grid_l.addLayout( qtx.QXHBoxLayout([q_stream_addr, qtx.QXLabel(text=':'), q_stream_port]), row, 1, 1, 2, alignment=qtx.AlignLeft | qtx.AlignVCenter ) + row += 1 super().__init__(backend, L('@QStreamOutput.module_title'), layout=grid_l) diff --git a/apps/DeepFaceLive/ui/widgets/QLineEditCSWText.py b/apps/DeepFaceLive/ui/widgets/QLineEditCSWText.py new file mode 100644 index 0000000..bc76d32 --- /dev/null +++ b/apps/DeepFaceLive/ui/widgets/QLineEditCSWText.py @@ -0,0 +1,45 @@ +from pathlib import Path + +from resources.fonts import QXFontDB +from resources.gfx import QXImageDB +from xlib import qt as qtx +from xlib.mp import csw as lib_csw + +from .QCSWControl import QCSWControl + + +class QLineEditCSWText(QCSWControl): + def __init__(self, csw_text : lib_csw.Text.Client, + font = None, + reflect_state_widgets=None): + """ + Implements lib_csw.Text control as LineEdit + """ + if not isinstance(csw_text, lib_csw.Text.Client): + raise ValueError('csw_path must be an instance of Text.Client') + + self._csw_text = csw_text + self._dlg = None + + csw_text.call_on_text(self._on_csw_text) + + if font is None: + font = QXFontDB.get_default_font() + lineedit = self._lineedit = qtx.QXLineEdit(font=font, + placeholder_text='...', + size_policy=('expanding', 'fixed'), + editingFinished=self.on_lineedit_editingFinished) + + super().__init__(csw_control=csw_text, reflect_state_widgets=reflect_state_widgets, + layout=qtx.QXHBoxLayout([lineedit]) ) + + def _on_csw_text(self, text): + + with qtx.BlockSignals(self._lineedit): + self._lineedit.setText(text) + + def on_lineedit_editingFinished(self): + text = self._lineedit.text() + if len(text) == 0: + text = None + self._csw_text.set_text(text) diff --git a/doc/setup_tutorial_windows/Media_source.png b/doc/setup_tutorial_windows/Media_source.png new file mode 100644 index 0000000000000000000000000000000000000000..21706800d71d7aceb85ac7b555ad0aa6c6514183 GIT binary patch literal 2760 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 O0000K^xqs)JSW63|{Q}Yg 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 JPDHLkV1m1)=PCdI literal 0 HcmV?d00001 diff --git a/doc/setup_tutorial_windows/setup_for_streaming.md b/doc/setup_tutorial_windows/setup_for_streaming.md index f18a9a4..433d6a2 100644 --- a/doc/setup_tutorial_windows/setup_for_streaming.md +++ b/doc/setup_tutorial_windows/setup_for_streaming.md @@ -9,7 +9,7 @@ DeepFaceLive only provides a video window of the replaced face. Since the face m So, what do we need for streaming? -Capture window and sound with some delay, transmit to streaming service (e.g. twitch, youtube, ...) +Capture window (or receive mpegts udp stream) and sound with some delay, transmit to streaming service (e.g. twitch, youtube, ...) Below, one of the solutions. @@ -63,8 +63,27 @@ Below, one of the solutions. -### DONE ! +### **DONE** ! ### Now you can stream yourself to a stream service. + + + +### instead of **Window capture** + +you can use **Media Source** + + + +with configuration: + + + +enable mpegts in **_Stream Output_** + + + + + \ No newline at end of file diff --git a/xlib/streamer/FFMPEGStreamer.py b/xlib/streamer/FFMPEGStreamer.py new file mode 100644 index 0000000..b184078 --- /dev/null +++ b/xlib/streamer/FFMPEGStreamer.py @@ -0,0 +1,52 @@ +import numpy as np + +from .. import ffmpeg as lib_ffmpeg + + +class FFMPEGStreamer: + def __init__(self): + self._ffmpeg_proc = None + self._addr = '127.0.0.1' + self._port = 1234 + self._width = 320 + self._height = 240 + + def set_addr_port(self, addr : str, port : int): + if self._addr != addr or self._port != port: + self._addr = addr + self._port = port + self.stop() + + def stop(self): + if self._ffmpeg_proc is not None: + self._ffmpeg_proc.kill() + self._ffmpeg_proc = None + + def _restart(self): + self.stop() + args = ['-y', '-re', + '-f', 'rawvideo', + '-vcodec','rawvideo', + '-pix_fmt', 'bgr24', + '-s', f'{self._width}:{self._height}', + '-i', '-', + '-f', 'mpegts', + '-q:v', '2', + f'udp://{self._addr}:{self._port}' + ] + self._ffmpeg_proc = lib_ffmpeg.run (args, pipe_stdin=True, quiet_stderr=True)#, pipe_stderr=True) + + def push_frame(self, img : np.ndarray): + H,W,C = img.shape + if self._width != W or self._height != H: + self._width = W + self._height = H + self.stop() + + if self._ffmpeg_proc is None: + self._restart() + + try: + self._ffmpeg_proc.stdin.write(img) + except: + self.stop() diff --git a/xlib/streamer/__init__.py b/xlib/streamer/__init__.py new file mode 100644 index 0000000..9efbce6 --- /dev/null +++ b/xlib/streamer/__init__.py @@ -0,0 +1 @@ +from .FFMPEGStreamer import FFMPEGStreamer \ No newline at end of file