StreamOutput: added mpegts udp output

This commit is contained in:
iperov 2022-05-08 14:57:15 +04:00
commit 5e75e7822d
9 changed files with 203 additions and 25 deletions

View file

@ -10,6 +10,7 @@ from xlib import os as lib_os
from xlib import time as lib_time from xlib import time as lib_time
from xlib.image import ImageProcessor from xlib.image import ImageProcessor
from xlib.mp import csw as lib_csw from xlib.mp import csw as lib_csw
from xlib.streamer import FFMPEGStreamer
from .BackendBase import (BackendConnection, BackendDB, BackendHost, from .BackendBase import (BackendConnection, BackendDB, BackendHost,
BackendSignal, BackendWeakHeap, BackendWorker, BackendSignal, BackendWeakHeap, BackendWorker,
@ -43,16 +44,17 @@ class SourceType(IntEnum):
SOURCE_N_MERGED_FRAME = 5 SOURCE_N_MERGED_FRAME = 5
SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME = 6 SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME = 6
ViewModeNames = ['@StreamOutput.SourceType.SOURCE_FRAME', ViewModeNames = ['@StreamOutput.SourceType.SOURCE_FRAME',
'@StreamOutput.SourceType.ALIGNED_FACE', '@StreamOutput.SourceType.ALIGNED_FACE',
'@StreamOutput.SourceType.SWAPPED_FACE', '@StreamOutput.SourceType.SWAPPED_FACE',
'@StreamOutput.SourceType.MERGED_FRAME', '@StreamOutput.SourceType.MERGED_FRAME',
'@StreamOutput.SourceType.MERGED_FRAME_OR_SOURCE_FRAME', '@StreamOutput.SourceType.MERGED_FRAME_OR_SOURCE_FRAME',
'@StreamOutput.SourceType.SOURCE_N_MERGED_FRAME', '@StreamOutput.SourceType.SOURCE_N_MERGED_FRAME',
'@StreamOutput.SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME', '@StreamOutput.SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME',
] ]
class StreamOutputWorker(BackendWorker): class StreamOutputWorker(BackendWorker):
def get_state(self) -> 'WorkerState': return super().get_state() def get_state(self) -> 'WorkerState': return super().get_state()
def get_control_sheet(self) -> 'Sheet.Worker': return super().get_control_sheet() 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_name = 'DeepFaceLive output'
self._wnd_showing = False self._wnd_showing = False
self._streamer = FFMPEGStreamer()
lib_os.set_timer_resolution(1) lib_os.set_timer_resolution(1)
state, cs = self.get_state(), self.get_control_sheet() 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.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_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.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.enable()
cs.source_type.set_choices(SourceType, ViewModeNames, none_choice_name='@misc.menu_select') cs.source_type.set_choices(SourceType, ViewModeNames, none_choice_name='@misc.menu_select')
cs.source_type.select(state.source_type) cs.source_type.select(state.source_type)
@ -107,16 +114,26 @@ class StreamOutputWorker(BackendWorker):
state.is_showing_window = not state.is_showing_window state.is_showing_window = not state.is_showing_window
cs.show_hide_window.signal() cs.show_hide_window.signal()
cs.save_sequence_path.enable() 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_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_sequence_path.set_paths(state.sequence_path)
cs.save_fill_frame_gap.enable() 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.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): def on_cs_source_type(self, idx, source_type):
state, cs = self.get_state(), self.get_control_sheet() state, cs = self.get_state(), self.get_control_sheet()
if source_type == SourceType.ALIGNED_FACE: if source_type == SourceType.ALIGNED_FACE:
@ -126,10 +143,10 @@ class StreamOutputWorker(BackendWorker):
else: else:
cs.aligned_face_id.disable() cs.aligned_face_id.disable()
state.source_type = source_type state.source_type = source_type
self.save_state() self.save_state()
self.reemit_frame_signal.send() self.reemit_frame_signal.send()
def show_window(self): def show_window(self):
state, cs = self.get_state(), self.get_control_sheet() state, cs = self.get_state(), self.get_control_sheet()
cv2.namedWindow(self._wnd_name) cv2.namedWindow(self._wnd_name)
@ -189,6 +206,23 @@ class StreamOutputWorker(BackendWorker):
state.save_fill_frame_gap = save_fill_frame_gap state.save_fill_frame_gap = save_fill_frame_gap
self.save_state() 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): def on_tick(self):
cs, state = self.get_control_sheet(), self.get_state() cs, state = self.get_control_sheet(), self.get_state()
@ -204,7 +238,9 @@ class StreamOutputWorker(BackendWorker):
source_type = state.source_type source_type = state.source_type
if source_type is not None and \ 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 buffered_frames = self.buffered_frames
view_image = None view_image = None
@ -213,7 +249,7 @@ class StreamOutputWorker(BackendWorker):
view_image = bcd.get_image(bcd.get_frame_image_name()) view_image = bcd.get_image(bcd.get_frame_image_name())
elif source_type in [SourceType.MERGED_FRAME, SourceType.MERGED_FRAME_OR_SOURCE_FRAME]: elif source_type in [SourceType.MERGED_FRAME, SourceType.MERGED_FRAME_OR_SOURCE_FRAME]:
view_image = bcd.get_image(bcd.get_merged_image_name()) 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()) view_image = bcd.get_image(bcd.get_frame_image_name())
elif source_type == SourceType.ALIGNED_FACE: elif source_type == SourceType.ALIGNED_FACE:
@ -228,17 +264,17 @@ class StreamOutputWorker(BackendWorker):
view_image = bcd.get_image(fsi.face_swap_image_name) view_image = bcd.get_image(fsi.face_swap_image_name)
if view_image is not None: if view_image is not None:
break break
elif source_type in [SourceType.SOURCE_N_MERGED_FRAME, SourceType.SOURCE_N_MERGED_FRAME_OR_SOURCE_FRAME]: 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()) source_frame = bcd.get_image(bcd.get_frame_image_name())
if source_frame is not None: if source_frame is not None:
source_frame = ImageProcessor(source_frame).to_ufloat32().get_image('HWC') source_frame = ImageProcessor(source_frame).to_ufloat32().get_image('HWC')
merged_frame = bcd.get_image(bcd.get_merged_image_name()) 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 merged_frame = source_frame
if source_frame is not None and merged_frame is not None: if source_frame is not None and merged_frame is not None:
view_image = np.concatenate( (source_frame, merged_frame), 1 ) view_image = np.concatenate( (source_frame, merged_frame), 1 )
@ -259,8 +295,13 @@ class StreamOutputWorker(BackendWorker):
pr = buffered_frames.process() pr = buffered_frames.process()
img = pr.new_data img = pr.new_data
if state.is_showing_window and img is not None: if img is not None:
cv2.imshow(self._wnd_name, img) 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: if state.is_showing_window:
cv2.waitKey(1) cv2.waitKey(1)
@ -277,7 +318,10 @@ class Sheet:
self.save_sequence_path = lib_csw.Paths.Client() self.save_sequence_path = lib_csw.Paths.Client()
self.save_sequence_path_error = lib_csw.Error.Client() self.save_sequence_path_error = lib_csw.Error.Client()
self.save_fill_frame_gap = lib_csw.Flag.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): class Worker(lib_csw.Sheet.Worker):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -289,7 +333,10 @@ class Sheet:
self.save_sequence_path = lib_csw.Paths.Host() self.save_sequence_path = lib_csw.Paths.Host()
self.save_sequence_path_error = lib_csw.Error.Host() self.save_sequence_path_error = lib_csw.Error.Host()
self.save_fill_frame_gap = lib_csw.Flag.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): class WorkerState(BackendWorkerState):
source_type : SourceType = None source_type : SourceType = None
is_showing_window : bool = None is_showing_window : bool = None
@ -297,3 +344,6 @@ class WorkerState(BackendWorkerState):
target_delay : int = None target_delay : int = None
sequence_path : Path = None sequence_path : Path = None
save_fill_frame_gap : bool = None save_fill_frame_gap : bool = None
is_streaming : bool = None
stream_addr : str = None
stream_port : int = None

View file

@ -1,5 +1,7 @@
from localization import L from localization import L
from resources.fonts import QXFontDB
from xlib import qt as qtx from xlib import qt as qtx
from xlib.qt.widgets.QXLabel import QXLabel
from ..backend import StreamOutput from ..backend import StreamOutput
from .widgets.QBackendPanel import QBackendPanel from .widgets.QBackendPanel import QBackendPanel
@ -9,6 +11,7 @@ from .widgets.QComboBoxCSWDynamicSingleSwitch import \
from .widgets.QErrorCSWError import QErrorCSWError from .widgets.QErrorCSWError import QErrorCSWError
from .widgets.QLabelCSWNumber import QLabelCSWNumber from .widgets.QLabelCSWNumber import QLabelCSWNumber
from .widgets.QLabelPopupInfo import QLabelPopupInfo from .widgets.QLabelPopupInfo import QLabelPopupInfo
from .widgets.QLineEditCSWText import QLineEditCSWText
from .widgets.QPathEditCSWPaths import QPathEditCSWPaths from .widgets.QPathEditCSWPaths import QPathEditCSWPaths
from .widgets.QSpinBoxCSWNumber import QSpinBoxCSWNumber from .widgets.QSpinBoxCSWNumber import QSpinBoxCSWNumber
from .widgets.QXPushButtonCSWSignal import QXPushButtonCSWSignal 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_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_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) grid_l = qtx.QXGridLayout(spacing=5)
row = 0 row = 0
grid_l.addWidget(q_average_fps_label, row, 0, 1, 1, alignment=qtx.AlignRight | qtx.AlignVCenter ) 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 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 ) 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 row += 1
grid_l.addWidget(q_save_sequence_path_error, row, 0, 1, 3) grid_l.addWidget(q_save_sequence_path_error, row, 0, 1, 3)
row += 1 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'), super().__init__(backend, L('@QStreamOutput.module_title'),
layout=grid_l) layout=grid_l)

View file

@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -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? 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. Below, one of the solutions.
@ -63,8 +63,27 @@ Below, one of the solutions.
</td></tr> </td></tr>
<tr><td colspan=2 align="center"> <tr><td colspan=2 align="center">
### DONE ! ### **DONE** !
### Now you can stream yourself to a stream service. ### Now you can stream yourself to a stream service.
</td></tr> </td></tr>
<tr><td colspan=2 align="center">
### instead of **Window capture**
you can use **Media Source**
<img src="media_source.png"></img>
with configuration:
<img src="media_source_config.png"></img>
enable mpegts in **_Stream Output_**
<img src="Media_source_stream_output.png"></img>
</td></tr>
</table> </table>

View file

@ -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()

View file

@ -0,0 +1 @@
from .FFMPEGStreamer import FFMPEGStreamer