mirror of
https://github.com/facefusion/facefusion.git
synced 2026-07-18 07:57:22 +02:00
Migrate to WHIP (#1120)
* migrate to whip part1 * migrate to whip part2 * migrate to whip part3 * migrate to whip part4 * migrate to whip/whep with bidirectional * migrate to whip/whep with bidirectional * use next library * add _next to lid datachannel files * cleanup and add todos * use internal helper rtcGetPayloadTypesForCodec * fix lint * refactor decode() * move logic to codecs * move logic to codecs * break encoders and decoders into multiple files * break encoders and decoders into multiple files * cleanup more * drop action for stream endpoints, keep type for self documentation * restore the v4 store * fix: align frame_width and frame_height to even in both collect() and read_resolution() in both decoders. --------- Co-authored-by: harisreedhar <h4harisreedhar.s.s@gmail.com>
This commit is contained in:
+21
-35
@@ -1,19 +1,18 @@
|
||||
import tempfile
|
||||
import threading
|
||||
from functools import partial
|
||||
from typing import Iterator
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from facefusion import metadata, rtc, session_manager, state_manager
|
||||
from facefusion import metadata, rtc, rtc_store, session_manager, state_manager
|
||||
from facefusion.apis import asset_store
|
||||
from facefusion.apis.core import create_api
|
||||
from facefusion.core import common_pre_check
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.hash_helper import create_hash
|
||||
from facefusion.libraries import datachannel as datachannel_module
|
||||
from facefusion.types import VideoCodec
|
||||
from .assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
|
||||
@@ -37,6 +36,7 @@ def before_all() -> None:
|
||||
def before_each() -> None:
|
||||
session_manager.SESSIONS.clear()
|
||||
asset_store.clear()
|
||||
rtc_store.clear()
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'module')
|
||||
@@ -45,16 +45,6 @@ def test_client() -> Iterator[TestClient]:
|
||||
yield test_client
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'function')
|
||||
def create_event() -> threading.Event:
|
||||
return threading.Event()
|
||||
|
||||
|
||||
@pytest.mark.helper
|
||||
def set_event(session_id : str, media_buffer : bytes, timestamp : int, event : threading.Event) -> None:
|
||||
event.set()
|
||||
|
||||
|
||||
def test_stream_image(test_client : TestClient) -> None:
|
||||
create_session_response = test_client.post('/session', json =
|
||||
{
|
||||
@@ -85,7 +75,7 @@ def test_stream_image(test_client : TestClient) -> None:
|
||||
|
||||
assert select_response.status_code == 200
|
||||
|
||||
with test_client.websocket_connect('/stream?mode=image', subprotocols =
|
||||
with test_client.websocket_connect('/stream?type=image&action=process', subprotocols =
|
||||
[
|
||||
'access_token.' + access_token
|
||||
]) as websocket:
|
||||
@@ -96,7 +86,7 @@ def test_stream_image(test_client : TestClient) -> None:
|
||||
|
||||
|
||||
@pytest.mark.parametrize('video_codec', [ 'av1', 'vp8' ])
|
||||
def test_stream_video(test_client : TestClient, create_event : threading.Event, video_codec : str) -> None:
|
||||
def test_stream_video(test_client : TestClient, video_codec : VideoCodec) -> None:
|
||||
create_session_response = test_client.post('/session', json =
|
||||
{
|
||||
'client_version': metadata.get('version')
|
||||
@@ -124,27 +114,23 @@ def test_stream_video(test_client : TestClient, create_event : threading.Event,
|
||||
'Authorization': 'Bearer ' + access_token
|
||||
})
|
||||
|
||||
with patch('facefusion.rtc.send_video_to_peers', side_effect = partial(set_event, event = create_event)):
|
||||
with test_client.websocket_connect('/stream?mode=video&codec=' + video_codec, subprotocols =
|
||||
[
|
||||
'access_token.' + access_token
|
||||
]) as websocket:
|
||||
websocket.send_bytes(chr(1).encode() + source_content)
|
||||
websocket.receive_text()
|
||||
peer_connection = rtc.create_peer_connection()
|
||||
|
||||
peer_connection = rtc.create_peer_connection()
|
||||
rtc.add_video_track(peer_connection, 'recvonly', 'vp8', 96)
|
||||
rtc.add_audio_track(peer_connection, 'recvonly', 'opus', 111)
|
||||
sdp_offer = rtc.create_sdp_offer(peer_connection)
|
||||
datachannel_module.create_static_library().rtcDeletePeerConnection(peer_connection)
|
||||
stream_response = test_client.post('/stream', content = sdp_offer, headers =
|
||||
{
|
||||
'Authorization': 'Bearer ' + access_token,
|
||||
'Content-Type': 'application/sdp'
|
||||
})
|
||||
if video_codec == 'av1':
|
||||
rtc.add_video_track(peer_connection, 'sendrecv', video_codec, 35)
|
||||
if video_codec == 'vp8':
|
||||
rtc.add_video_track(peer_connection, 'sendrecv', video_codec, 96)
|
||||
|
||||
assert stream_response.status_code == 201
|
||||
rtc.add_audio_track(peer_connection, 'sendrecv', 'opus', 111)
|
||||
sdp_offer = rtc.create_sdp_offer(peer_connection)
|
||||
datachannel_module.create_static_library().rtcDeletePeerConnection(peer_connection)
|
||||
|
||||
create_event.wait(timeout = 10)
|
||||
with patch('facefusion.rtc.send_video'):
|
||||
stream_response = test_client.post('/stream?type=video&action=process', content = sdp_offer, headers =
|
||||
{
|
||||
'Authorization': 'Bearer ' + access_token,
|
||||
'Content-Type': 'application/sdp'
|
||||
})
|
||||
|
||||
assert create_event.is_set()
|
||||
assert stream_response.status_code == 201
|
||||
assert 'm=video' in stream_response.text
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import cv2
|
||||
import pytest
|
||||
from tests.assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.codecs.aom_decoder import create, decode, destroy, read_resolution
|
||||
from facefusion.codecs.aom_encoder import create as create_encoder, encode
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.libraries import aom as aom_module
|
||||
from facefusion.vision import read_video_frame
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'module', autouse = True)
|
||||
def before_all() -> None:
|
||||
state_manager.init_item('download_providers', [ 'github', 'huggingface' ])
|
||||
|
||||
conditional_download(get_test_examples_directory(), [ 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' ])
|
||||
|
||||
aom_module.pre_check()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_create() -> None:
|
||||
assert create()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_decode() -> None:
|
||||
vision_frame = read_video_frame(get_test_example_file('target-240p.mp4'))
|
||||
video_buffer = cv2.cvtColor(vision_frame, cv2.COLOR_BGR2YUV_I420).tobytes()
|
||||
video_resolution = (vision_frame.shape[1], vision_frame.shape[0])
|
||||
aom_encoder = create_encoder(video_resolution, 1000, 1, 0)
|
||||
encoded_buffer = encode(aom_encoder, video_buffer, video_resolution, 0)
|
||||
decode_resolution = read_resolution(create(), encoded_buffer)
|
||||
|
||||
assert len(decode(create(), encoded_buffer)) == decode_resolution[0] * decode_resolution[1] * 3 // 2
|
||||
assert decode(create(), bytes()) == bytes()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_read_resolution() -> None:
|
||||
vision_frame = read_video_frame(get_test_example_file('target-240p.mp4'))
|
||||
video_buffer = cv2.cvtColor(vision_frame, cv2.COLOR_BGR2YUV_I420).tobytes()
|
||||
video_resolution = (vision_frame.shape[1], vision_frame.shape[0])
|
||||
aom_encoder = create_encoder(video_resolution, 1000, 1, 0)
|
||||
encoded_buffer = encode(aom_encoder, video_buffer, video_resolution, 0)
|
||||
|
||||
assert read_resolution(create(), encoded_buffer)[0] >= video_resolution[0]
|
||||
assert read_resolution(create(), encoded_buffer)[1] >= video_resolution[1]
|
||||
assert read_resolution(create(), bytes()) is None
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_destroy() -> None:
|
||||
aom_decoder = create()
|
||||
|
||||
with patch.object(aom_module.create_static_library(), 'aom_codec_destroy') as mock:
|
||||
destroy(aom_decoder)
|
||||
mock.assert_called_once_with(aom_decoder)
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from tests.assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.codecs.aom import create_aom_encoder, destroy_aom_encoder, encode_aom_buffer
|
||||
from facefusion.codecs.aom_encoder import create, destroy, encode
|
||||
from facefusion.common_helper import is_linux, is_macos, is_windows
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.hash_helper import create_hash
|
||||
@@ -22,27 +22,27 @@ def before_all() -> None:
|
||||
aom_module.pre_check()
|
||||
|
||||
|
||||
def test_create_aom_encoder() -> None:
|
||||
assert create_aom_encoder(1000, 8, 16, (320, 240))
|
||||
assert create_aom_encoder(0, 0, 0, (0, 0)) is None
|
||||
def test_create() -> None:
|
||||
assert create((320, 240), 1000, 8, 16)
|
||||
assert create((0, 0), 0, 0, 0) is None
|
||||
|
||||
|
||||
def test_encode_aom_buffer() -> None:
|
||||
def test_encode() -> None:
|
||||
vision_frame = read_video_frame(get_test_example_file('target-240p.mp4'))
|
||||
video_buffer = cv2.cvtColor(vision_frame, cv2.COLOR_BGR2YUV_I420).tobytes()
|
||||
video_resolution = (vision_frame.shape[1], vision_frame.shape[0])
|
||||
aom_encoder = create_aom_encoder(1000, 1, 0, video_resolution)
|
||||
aom_encoder = create(video_resolution, 1000, 1, 0)
|
||||
|
||||
if is_linux() or is_windows():
|
||||
assert create_hash(encode_aom_buffer(aom_encoder, video_buffer, video_resolution, 3)) == '3ab6cc31'
|
||||
assert create_hash(encode(aom_encoder, video_buffer, video_resolution, 3)) == '3ab6cc31'
|
||||
|
||||
if is_macos():
|
||||
pytest.skip()
|
||||
|
||||
|
||||
def test_destroy_aom_encoder() -> None:
|
||||
aom_encoder = create_aom_encoder(1000, 8, 16, (320, 240))
|
||||
def test_destroy() -> None:
|
||||
aom_encoder = create((320, 240), 1000, 8, 16)
|
||||
|
||||
with patch.object(aom_module.create_static_library(), 'aom_codec_destroy') as mock:
|
||||
destroy_aom_encoder(aom_encoder)
|
||||
destroy(aom_encoder)
|
||||
mock.assert_called_once_with(aom_encoder)
|
||||
@@ -0,0 +1,47 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy
|
||||
import pytest
|
||||
from tests.assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.codecs.opus_decoder import create, decode, destroy
|
||||
from facefusion.codecs.opus_encoder import create as create_encoder, encode
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.ffmpeg import read_audio_buffer
|
||||
from facefusion.libraries import opus as opus_module
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'module', autouse = True)
|
||||
def before_all() -> None:
|
||||
state_manager.init_item('download_providers', [ 'github', 'huggingface' ])
|
||||
|
||||
conditional_download(get_test_examples_directory(), [ 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/source.mp3' ])
|
||||
|
||||
opus_module.pre_check()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_create() -> None:
|
||||
assert create(48000, 2)
|
||||
assert create(0, 0) is None
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_decode() -> None:
|
||||
audio_buffer = read_audio_buffer(get_test_example_file('source.mp3'), 48000, 16, 2)
|
||||
audio_sample = numpy.frombuffer(audio_buffer, dtype = numpy.int16).astype(numpy.float32) / 32768.0
|
||||
opus_encoder = create_encoder(48000, 2)
|
||||
encoded_buffer = encode(opus_encoder, audio_sample.tobytes(), 960)
|
||||
opus_decoder = create(48000, 2)
|
||||
|
||||
assert len(decode(opus_decoder, encoded_buffer, 960, 2)) == 960 * 2 * 4
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_destroy() -> None:
|
||||
opus_decoder = create(48000, 2)
|
||||
|
||||
with patch.object(opus_module.create_static_library(), 'opus_decoder_destroy') as mock:
|
||||
destroy(opus_decoder)
|
||||
mock.assert_called_once_with(opus_decoder)
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from tests.assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.codecs.opus import create_opus_encoder, destroy_opus_encoder, encode_opus_buffer
|
||||
from facefusion.codecs.opus_encoder import create, destroy, encode
|
||||
from facefusion.common_helper import is_linux, is_macos, is_windows
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.ffmpeg import read_audio_buffer
|
||||
@@ -22,26 +22,26 @@ def before_all() -> None:
|
||||
opus_module.pre_check()
|
||||
|
||||
|
||||
def test_create_opus_encoder() -> None:
|
||||
assert create_opus_encoder(48000, 2)
|
||||
assert create_opus_encoder(0, 0) is None
|
||||
def test_create() -> None:
|
||||
assert create(48000, 2)
|
||||
assert create(0, 0) is None
|
||||
|
||||
|
||||
def test_encode_opus_buffer() -> None:
|
||||
def test_encode() -> None:
|
||||
audio_buffer = read_audio_buffer(get_test_example_file('source.mp3'), 48000, 16, 2)
|
||||
audio_sample = numpy.frombuffer(audio_buffer, dtype = numpy.int16).astype(numpy.float32) / 32768.0
|
||||
opus_encoder = create_opus_encoder(48000, 2)
|
||||
opus_encoder = create(48000, 2)
|
||||
|
||||
if is_linux() or is_windows():
|
||||
assert create_hash(encode_opus_buffer(opus_encoder, audio_sample.tobytes(), 960)) == '8abe71cf'
|
||||
assert create_hash(encode(opus_encoder, audio_sample.tobytes(), 960)) == '8abe71cf'
|
||||
|
||||
if is_macos():
|
||||
pytest.skip()
|
||||
|
||||
|
||||
def test_destroy_opus_encoder() -> None:
|
||||
opus_encoder = create_opus_encoder(48000, 2)
|
||||
def test_destroy() -> None:
|
||||
opus_encoder = create(48000, 2)
|
||||
|
||||
with patch.object(opus_module.create_static_library(), 'opus_encoder_destroy') as mock:
|
||||
destroy_opus_encoder(opus_encoder)
|
||||
destroy(opus_encoder)
|
||||
mock.assert_called_once_with(opus_encoder)
|
||||
@@ -0,0 +1,61 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import cv2
|
||||
import pytest
|
||||
from tests.assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.codecs.vpx_decoder import create, decode, destroy, read_resolution
|
||||
from facefusion.codecs.vpx_encoder import create as create_encoder, encode
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.libraries import vpx as vpx_module
|
||||
from facefusion.vision import read_video_frame
|
||||
|
||||
|
||||
@pytest.fixture(scope = 'module', autouse = True)
|
||||
def before_all() -> None:
|
||||
state_manager.init_item('download_providers', [ 'github', 'huggingface' ])
|
||||
|
||||
conditional_download(get_test_examples_directory(), [ 'https://github.com/facefusion/facefusion-assets/releases/download/examples-3.0.0/target-240p.mp4' ])
|
||||
|
||||
vpx_module.pre_check()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_create() -> None:
|
||||
assert create()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_decode() -> None:
|
||||
vision_frame = read_video_frame(get_test_example_file('target-240p.mp4'))
|
||||
video_buffer = cv2.cvtColor(vision_frame, cv2.COLOR_BGR2YUV_I420).tobytes()
|
||||
video_resolution = (vision_frame.shape[1], vision_frame.shape[0])
|
||||
vpx_encoder = create_encoder(video_resolution, 1000, 1, 0)
|
||||
encoded_buffer = encode(vpx_encoder, video_buffer, video_resolution, 0)
|
||||
vpx_decoder = create()
|
||||
|
||||
assert len(decode(vpx_decoder, encoded_buffer)) == video_resolution[0] * video_resolution[1] * 3 // 2
|
||||
assert decode(vpx_decoder, bytes()) == bytes()
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_read_resolution() -> None:
|
||||
vision_frame = read_video_frame(get_test_example_file('target-240p.mp4'))
|
||||
video_buffer = cv2.cvtColor(vision_frame, cv2.COLOR_BGR2YUV_I420).tobytes()
|
||||
video_resolution = (vision_frame.shape[1], vision_frame.shape[0])
|
||||
vpx_encoder = create_encoder(video_resolution, 1000, 1, 0)
|
||||
encoded_buffer = encode(vpx_encoder, video_buffer, video_resolution, 0)
|
||||
vpx_decoder = create()
|
||||
|
||||
assert read_resolution(vpx_decoder, encoded_buffer) == video_resolution
|
||||
assert read_resolution(vpx_decoder, bytes()) is None
|
||||
|
||||
|
||||
#TODO: needs review
|
||||
def test_destroy() -> None:
|
||||
vpx_decoder = create()
|
||||
|
||||
with patch.object(vpx_module.create_static_library(), 'vpx_codec_destroy') as mock:
|
||||
destroy(vpx_decoder)
|
||||
mock.assert_called_once_with(vpx_decoder)
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from tests.assert_helper import get_test_example_file, get_test_examples_directory
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.codecs.vpx import create_vpx_encoder, destroy_vpx_encoder, encode_vpx_buffer
|
||||
from facefusion.codecs.vpx_encoder import create, destroy, encode
|
||||
from facefusion.common_helper import is_linux, is_macos, is_windows
|
||||
from facefusion.download import conditional_download
|
||||
from facefusion.hash_helper import create_hash
|
||||
@@ -22,27 +22,27 @@ def before_all() -> None:
|
||||
vpx_module.pre_check()
|
||||
|
||||
|
||||
def test_create_vpx_encoder() -> None:
|
||||
assert create_vpx_encoder(1000, 8, 16, (320, 240))
|
||||
assert create_vpx_encoder(0, 0, 0, (0, 0)) is None
|
||||
def test_create() -> None:
|
||||
assert create((320, 240), 1000, 8, 16)
|
||||
assert create((0, 0), 0, 0, 0) is None
|
||||
|
||||
|
||||
def test_encode_vpx_buffer() -> None:
|
||||
def test_encode() -> None:
|
||||
vision_frame = read_video_frame(get_test_example_file('target-240p.mp4'))
|
||||
video_buffer = cv2.cvtColor(vision_frame, cv2.COLOR_BGR2YUV_I420).tobytes()
|
||||
video_resolution = (vision_frame.shape[1], vision_frame.shape[0])
|
||||
vpx_encoder = create_vpx_encoder(1000, 1, 0, video_resolution)
|
||||
vpx_encoder = create(video_resolution, 1000, 1, 0)
|
||||
|
||||
if is_linux() or is_windows():
|
||||
assert create_hash(encode_vpx_buffer(vpx_encoder, video_buffer, video_resolution, 3)) == 'ce133a1f'
|
||||
assert create_hash(encode(vpx_encoder, video_buffer, video_resolution, 3)) == 'ce133a1f'
|
||||
|
||||
if is_macos():
|
||||
pytest.skip()
|
||||
|
||||
|
||||
def test_destroy_vpx_encoder() -> None:
|
||||
vpx_encoder = create_vpx_encoder(1000, 8, 16, (320, 240))
|
||||
def test_destroy() -> None:
|
||||
vpx_encoder = create((320, 240), 1000, 8, 16)
|
||||
|
||||
with patch.object(vpx_module.create_static_library(), 'vpx_codec_destroy') as mock:
|
||||
destroy_vpx_encoder(vpx_encoder)
|
||||
destroy(vpx_encoder)
|
||||
mock.assert_called_once_with(vpx_encoder)
|
||||
+39
-29
@@ -4,7 +4,7 @@ import pytest
|
||||
|
||||
from facefusion import state_manager
|
||||
from facefusion.libraries import datachannel as datachannel_module, opus as opus_module, vpx as vpx_module
|
||||
from facefusion.rtc import add_audio_track, add_video_track, create_peer_connection, create_sdp_answer, create_sdp_offer, delete_peers, detect_sdp_media, send_audio_to_peers, send_video_to_peers, set_remote_description
|
||||
from facefusion.rtc import add_audio_track, add_video_track, create_peer_connection, create_sdp_answer, create_sdp_offer, delete_peers, get_payload_type, send_audio, send_video, set_remote_description
|
||||
from facefusion.types import RtcPeer
|
||||
|
||||
|
||||
@@ -57,48 +57,56 @@ def test_create_sdp_answer() -> None:
|
||||
|
||||
assert 'm=video' in sdp_answer
|
||||
assert 'VP8/90000' in sdp_answer
|
||||
assert 'a=ssrc:42 cname:video' in sdp_answer
|
||||
assert 'm=audio' in sdp_answer
|
||||
assert 'opus/48000/2' in sdp_answer
|
||||
assert 'a=ssrc:43 cname:audio' in sdp_answer
|
||||
assert 'a=recvonly' in sdp_answer
|
||||
|
||||
assert datachannel_library.rtcDeletePeerConnection(sender_peer_connection) == 0
|
||||
assert datachannel_library.rtcDeletePeerConnection(receiver_peer_connection) == 0
|
||||
|
||||
|
||||
def test_send_audio_to_peers() -> None:
|
||||
def test_send_video() -> None:
|
||||
datachannel_library = datachannel_module.create_static_library()
|
||||
peer_connection = create_peer_connection()
|
||||
audio_track = add_audio_track(peer_connection, 'sendonly', 'opus', 111)
|
||||
rtc_peers : List[RtcPeer] =\
|
||||
[
|
||||
video_track = add_video_track(peer_connection, 'sendonly', 'vp8', 96)
|
||||
rtc_peer : RtcPeer =\
|
||||
{
|
||||
'peer_connection': peer_connection,
|
||||
'video':
|
||||
{
|
||||
'peer_connection': peer_connection,
|
||||
'video_track': 0,
|
||||
'audio_track': audio_track
|
||||
'sender_track': video_track,
|
||||
'receiver_track': video_track,
|
||||
'codec': 'vp8'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
send_audio_to_peers(rtc_peers, bytes(960), 0)
|
||||
send_video(rtc_peer, bytes(1024), 0)
|
||||
|
||||
datachannel_library.rtcDeletePeerConnection(peer_connection)
|
||||
|
||||
|
||||
def test_send_video_to_peers() -> None:
|
||||
def test_send_audio() -> None:
|
||||
datachannel_library = datachannel_module.create_static_library()
|
||||
peer_connection = create_peer_connection()
|
||||
video_track = add_video_track(peer_connection, 'sendonly', 'vp8', 96)
|
||||
rtc_peers : List[RtcPeer] =\
|
||||
[
|
||||
audio_track = add_audio_track(peer_connection, 'sendonly', 'opus', 111)
|
||||
rtc_peer : RtcPeer =\
|
||||
{
|
||||
'peer_connection': peer_connection,
|
||||
'video':
|
||||
{
|
||||
'peer_connection': peer_connection,
|
||||
'video_track': video_track,
|
||||
'audio_track': 0
|
||||
'sender_track': 0,
|
||||
'receiver_track': 0,
|
||||
'codec': 'vp8'
|
||||
},
|
||||
'audio':
|
||||
{
|
||||
'sender_track': audio_track,
|
||||
'receiver_track': audio_track,
|
||||
'codec': 'opus'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
send_video_to_peers(rtc_peers, bytes(1024), 0)
|
||||
send_audio(rtc_peer, bytes(960), 0)
|
||||
|
||||
datachannel_library.rtcDeletePeerConnection(peer_connection)
|
||||
|
||||
@@ -110,8 +118,12 @@ def test_delete_peers() -> None:
|
||||
[
|
||||
{
|
||||
'peer_connection': peer_connection,
|
||||
'video_track': 0,
|
||||
'audio_track': 0
|
||||
'video':
|
||||
{
|
||||
'sender_track': 0,
|
||||
'receiver_track': 0,
|
||||
'codec': 'vp8'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -120,16 +132,14 @@ def test_delete_peers() -> None:
|
||||
assert datachannel_library.rtcDeletePeerConnection(peer_connection) == -1
|
||||
|
||||
|
||||
def test_detect_sdp_media() -> None:
|
||||
def test_get_payload_type() -> None:
|
||||
peer_connection = create_peer_connection()
|
||||
add_video_track(peer_connection, 'sendonly', 'vp8', 96)
|
||||
add_audio_track(peer_connection, 'sendonly', 'opus', 111)
|
||||
sdp_offer = create_sdp_offer(peer_connection)
|
||||
sdp_payload = detect_sdp_media(sdp_offer)
|
||||
|
||||
assert sdp_payload.get('video').get('codec') == 'vp8'
|
||||
assert sdp_payload.get('video').get('payload_type') == 96
|
||||
assert sdp_payload.get('audio').get('codec') == 'opus'
|
||||
assert sdp_payload.get('audio').get('payload_type') == 111
|
||||
assert get_payload_type(sdp_offer, 'vp8') == 96
|
||||
assert get_payload_type(sdp_offer, 'opus') == 111
|
||||
assert get_payload_type(sdp_offer, 'av1') == 0
|
||||
|
||||
datachannel_module.create_static_library().rtcDeletePeerConnection(peer_connection)
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
import asyncio
|
||||
import queue
|
||||
from typing import Any, Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import cv2
|
||||
import numpy
|
||||
import pytest
|
||||
from numpy.typing import NDArray
|
||||
from starlette.websockets import WebSocketState
|
||||
|
||||
from facefusion.apis.stream_helper import encode_audio_loop, encode_video_loop, handle_video_stream
|
||||
from facefusion.hash_helper import create_hash
|
||||
from facefusion.types import VideoCodec, VisionFrame
|
||||
|
||||
|
||||
def _make_handler_websocket(events : list[Any]) -> MagicMock:
|
||||
mock = MagicMock()
|
||||
mock.scope = {}
|
||||
mock.client_state = WebSocketState.CONNECTED
|
||||
mock.accept = AsyncMock()
|
||||
mock.send_text = AsyncMock()
|
||||
mock.close = AsyncMock()
|
||||
mock.receive = AsyncMock(side_effect = events)
|
||||
return mock
|
||||
|
||||
|
||||
def _make_video_packet(frame : NDArray[Any]) -> bytes:
|
||||
_, encoded = cv2.imencode('.jpg', frame)
|
||||
return b'\x01' + encoded.tobytes()
|
||||
|
||||
|
||||
def _make_audio_packet(samples : NDArray[Any]) -> bytes:
|
||||
return b'\x02' + samples.tobytes()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('video_codec', [ 'av1', 'vp8' ])
|
||||
def test_encode_video_loop(video_codec : VideoCodec) -> None:
|
||||
frame = numpy.full((64, 64, 3), 128, dtype = numpy.uint8)
|
||||
small_frame = numpy.full((64, 64, 3), 128, dtype = numpy.uint8)
|
||||
large_frame = numpy.full((128, 128, 3), 128, dtype = numpy.uint8)
|
||||
black_frame = numpy.zeros((64, 64, 3), dtype = numpy.uint8)
|
||||
prefix = 'facefusion.apis.stream_helper.'
|
||||
|
||||
create_name = prefix + 'create_aom_encoder'
|
||||
encode_name = prefix + 'encode_aom_buffer'
|
||||
destroy_name = prefix + 'destroy_aom_encoder'
|
||||
|
||||
if video_codec == 'vp8':
|
||||
create_name = prefix + 'create_vpx_encoder'
|
||||
encode_name = prefix + 'encode_vpx_buffer'
|
||||
destroy_name = prefix + 'destroy_vpx_encoder'
|
||||
|
||||
vision_frame_queue : queue.Queue[Optional[VisionFrame]] = queue.Queue()
|
||||
vision_frame_queue.put(frame)
|
||||
vision_frame_queue.put(None)
|
||||
with patch(prefix + 'process_vision_frame', return_value = frame), \
|
||||
patch(create_name, return_value = MagicMock()), \
|
||||
patch(encode_name, return_value = b'encoded'), \
|
||||
patch(destroy_name), \
|
||||
patch(prefix + 'rtc_store') as mock_rtc_store, \
|
||||
patch(prefix + 'rtc') as mock_rtc:
|
||||
mock_rtc_store.get_peers.return_value = [ MagicMock() ]
|
||||
encode_video_loop(video_codec, vision_frame_queue, 'session-1', (64, 64))
|
||||
mock_rtc.send_video_to_peers.assert_called_once()
|
||||
|
||||
vision_frame_queue = queue.Queue()
|
||||
vision_frame_queue.put(frame)
|
||||
vision_frame_queue.put(frame)
|
||||
vision_frame_queue.put(frame)
|
||||
vision_frame_queue.put(None)
|
||||
with patch(prefix + 'process_vision_frame', return_value = frame), \
|
||||
patch(create_name, return_value = MagicMock()), \
|
||||
patch(encode_name, return_value = b'encoded'), \
|
||||
patch(destroy_name), \
|
||||
patch(prefix + 'rtc_store') as mock_rtc_store, \
|
||||
patch(prefix + 'rtc') as mock_rtc:
|
||||
mock_rtc_store.get_peers.return_value = [ MagicMock() ]
|
||||
encode_video_loop(video_codec, vision_frame_queue, 'session-1', (64, 64))
|
||||
assert mock_rtc.send_video_to_peers.call_count == 3
|
||||
|
||||
vision_frame_queue = queue.Queue()
|
||||
vision_frame_queue.put(black_frame)
|
||||
with patch(create_name, return_value = MagicMock()), \
|
||||
patch(destroy_name) as mock_destroy, \
|
||||
patch(prefix + 'rtc') as mock_rtc:
|
||||
encode_video_loop(video_codec, vision_frame_queue, 'session-1', (64, 64))
|
||||
mock_rtc.send_video_to_peers.assert_not_called()
|
||||
mock_destroy.assert_called_once()
|
||||
|
||||
vision_frame_queue = queue.Queue()
|
||||
vision_frame_queue.put(frame)
|
||||
vision_frame_queue.put(None)
|
||||
with patch(prefix + 'process_vision_frame', return_value = frame), \
|
||||
patch(create_name, return_value = MagicMock()), \
|
||||
patch(encode_name, return_value = b''), \
|
||||
patch(destroy_name), \
|
||||
patch(prefix + 'rtc_store'), \
|
||||
patch(prefix + 'rtc') as mock_rtc:
|
||||
encode_video_loop(video_codec, vision_frame_queue, 'session-1', (64, 64))
|
||||
mock_rtc.send_video_to_peers.assert_not_called()
|
||||
|
||||
vision_frame_queue = queue.Queue()
|
||||
vision_frame_queue.put(small_frame)
|
||||
vision_frame_queue.put(None)
|
||||
with patch(prefix + 'process_vision_frame', return_value = large_frame), \
|
||||
patch(create_name, return_value = MagicMock()) as mock_create, \
|
||||
patch(encode_name, return_value = b'encoded'), \
|
||||
patch(destroy_name) as mock_destroy, \
|
||||
patch(prefix + 'rtc_store') as mock_rtc_store, \
|
||||
patch(prefix + 'rtc') as mock_rtc:
|
||||
mock_rtc_store.get_peers.return_value = [ MagicMock() ]
|
||||
encode_video_loop(video_codec, vision_frame_queue, 'session-1', (64, 64))
|
||||
assert mock_create.call_count == 2
|
||||
assert mock_destroy.call_count == 2
|
||||
mock_rtc.send_video_to_peers.assert_called_once()
|
||||
|
||||
vision_frame_queue = queue.Queue()
|
||||
vision_frame_queue.put(frame)
|
||||
vision_frame_queue.put(None)
|
||||
with patch(create_name, return_value = None), \
|
||||
patch(prefix + 'rtc') as mock_rtc:
|
||||
encode_video_loop(video_codec, vision_frame_queue, 'session-1', (64, 64))
|
||||
mock_rtc.send_video_to_peers.assert_not_called()
|
||||
|
||||
|
||||
# TODO: refine test
|
||||
def test_encode_audio_loop() -> None:
|
||||
audio_chunk = numpy.zeros(1920, dtype = numpy.float32).tobytes()
|
||||
|
||||
audio_chunk_queue : queue.Queue[Optional[bytes]] = queue.Queue()
|
||||
audio_chunk_queue.put(audio_chunk)
|
||||
audio_chunk_queue.put(None)
|
||||
with patch('facefusion.apis.stream_helper.create_opus_encoder', return_value = MagicMock()), \
|
||||
patch('facefusion.apis.stream_helper.encode_opus_buffer', return_value = b'encoded'), \
|
||||
patch('facefusion.apis.stream_helper.destroy_opus_encoder'), \
|
||||
patch('facefusion.apis.stream_helper.rtc_store') as mock_rtc_store, \
|
||||
patch('facefusion.apis.stream_helper.rtc') as mock_rtc:
|
||||
mock_rtc_store.get_peers.return_value = [ MagicMock() ]
|
||||
encode_audio_loop('opus', audio_chunk_queue, 'session-1')
|
||||
mock_rtc.send_audio_to_peers.assert_called_once()
|
||||
assert mock_rtc.send_audio_to_peers.call_args[0][2] == 0
|
||||
|
||||
audio_chunk_queue = queue.Queue()
|
||||
audio_chunk_queue.put(audio_chunk)
|
||||
audio_chunk_queue.put(audio_chunk)
|
||||
audio_chunk_queue.put(None)
|
||||
with patch('facefusion.apis.stream_helper.create_opus_encoder', return_value = MagicMock()), \
|
||||
patch('facefusion.apis.stream_helper.encode_opus_buffer', return_value = b'encoded'), \
|
||||
patch('facefusion.apis.stream_helper.destroy_opus_encoder'), \
|
||||
patch('facefusion.apis.stream_helper.rtc_store') as mock_rtc_store, \
|
||||
patch('facefusion.apis.stream_helper.rtc') as mock_rtc:
|
||||
mock_rtc_store.get_peers.return_value = [ MagicMock() ]
|
||||
encode_audio_loop('opus', audio_chunk_queue, 'session-1')
|
||||
assert mock_rtc.send_audio_to_peers.call_count == 2
|
||||
assert mock_rtc.send_audio_to_peers.call_args_list[0][0][2] == 0
|
||||
assert mock_rtc.send_audio_to_peers.call_args_list[1][0][2] == 960
|
||||
|
||||
audio_chunk_queue = queue.Queue()
|
||||
audio_chunk_queue.put(audio_chunk)
|
||||
audio_chunk_queue.put(None)
|
||||
with patch('facefusion.apis.stream_helper.create_opus_encoder', return_value = MagicMock()), \
|
||||
patch('facefusion.apis.stream_helper.encode_opus_buffer', return_value = b''), \
|
||||
patch('facefusion.apis.stream_helper.destroy_opus_encoder'), \
|
||||
patch('facefusion.apis.stream_helper.rtc_store'), \
|
||||
patch('facefusion.apis.stream_helper.rtc') as mock_rtc:
|
||||
encode_audio_loop('opus', audio_chunk_queue, 'session-1')
|
||||
mock_rtc.send_audio_to_peers.assert_not_called()
|
||||
|
||||
audio_chunk_queue = queue.Queue()
|
||||
audio_chunk_queue.put(audio_chunk)
|
||||
audio_chunk_queue.put(None)
|
||||
with patch('facefusion.apis.stream_helper.create_opus_encoder', return_value = MagicMock()), \
|
||||
patch('facefusion.apis.stream_helper.encode_opus_buffer', return_value = None), \
|
||||
patch('facefusion.apis.stream_helper.destroy_opus_encoder') as mock_destroy, \
|
||||
patch('facefusion.apis.stream_helper.rtc_store'), \
|
||||
patch('facefusion.apis.stream_helper.rtc'):
|
||||
encode_audio_loop('opus', audio_chunk_queue, 'session-1')
|
||||
mock_destroy.assert_called_once()
|
||||
|
||||
audio_chunk_queue = queue.Queue()
|
||||
audio_chunk_queue.put(b'')
|
||||
with patch('facefusion.apis.stream_helper.create_opus_encoder', return_value = MagicMock()), \
|
||||
patch('facefusion.apis.stream_helper.destroy_opus_encoder') as mock_destroy, \
|
||||
patch('facefusion.apis.stream_helper.rtc') as mock_rtc:
|
||||
encode_audio_loop('opus', audio_chunk_queue, 'session-1')
|
||||
mock_rtc.send_audio_to_peers.assert_not_called()
|
||||
mock_destroy.assert_called_once()
|
||||
|
||||
|
||||
# TODO: refine test
|
||||
def test_handle_video_stream() -> None:
|
||||
frame = numpy.full((64, 64, 3), 128, dtype = numpy.uint8)
|
||||
video_packet = _make_video_packet(frame)
|
||||
audio_packet = _make_audio_packet(numpy.zeros(1920, dtype = numpy.float32))
|
||||
|
||||
websocket = _make_handler_websocket([ {'type': 'websocket.receive', 'bytes': video_packet}, {'type': 'websocket.disconnect'} ])
|
||||
with patch('facefusion.apis.stream_helper.get_sec_websocket_protocol', return_value = 'proto'), \
|
||||
patch('facefusion.apis.stream_helper.extract_access_token', return_value = 'token'), \
|
||||
patch('facefusion.apis.stream_helper.session_manager.find_session_id', return_value = 'session-1'), \
|
||||
patch('facefusion.apis.stream_helper.session_context.set_session_id'), \
|
||||
patch('facefusion.apis.stream_helper.state_manager.get_item', return_value = 30), \
|
||||
patch('facefusion.apis.stream_helper.encode_video_loop') as mock_loop, \
|
||||
patch('facefusion.apis.stream_helper.encode_audio_loop'), \
|
||||
patch('facefusion.apis.stream_helper.rtc_store') as mock_rtc:
|
||||
asyncio.run(handle_video_stream(websocket))
|
||||
websocket.accept.assert_called_once_with(subprotocol = 'proto')
|
||||
websocket.send_text.assert_called_once_with('ready')
|
||||
websocket.close.assert_called_once()
|
||||
mock_rtc.init_peers.assert_called_once_with('session-1')
|
||||
mock_rtc.delete_peers.assert_called_once_with('session-1')
|
||||
_, _, loop_session_id, loop_resolution = mock_loop.call_args[0]
|
||||
assert loop_session_id == 'session-1'
|
||||
assert loop_resolution == (64, 64)
|
||||
|
||||
websocket = _make_handler_websocket([ {'type': 'websocket.receive', 'bytes': video_packet}, {'type': 'websocket.disconnect'} ])
|
||||
with patch('facefusion.apis.stream_helper.get_sec_websocket_protocol', return_value = 'proto'), \
|
||||
patch('facefusion.apis.stream_helper.extract_access_token', return_value = 'token'), \
|
||||
patch('facefusion.apis.stream_helper.session_manager.find_session_id', return_value = None), \
|
||||
patch('facefusion.apis.stream_helper.session_context.set_session_id'), \
|
||||
patch('facefusion.apis.stream_helper.rtc_store') as mock_rtc:
|
||||
asyncio.run(handle_video_stream(websocket))
|
||||
websocket.accept.assert_called_once()
|
||||
websocket.send_text.assert_not_called()
|
||||
mock_rtc.init_peers.assert_not_called()
|
||||
|
||||
websocket = _make_handler_websocket([ {'type': 'websocket.receive', 'bytes': video_packet}, {'type': 'websocket.receive', 'bytes': audio_packet}, {'type': 'websocket.disconnect'} ])
|
||||
with patch('facefusion.apis.stream_helper.get_sec_websocket_protocol', return_value = 'proto'), \
|
||||
patch('facefusion.apis.stream_helper.extract_access_token', return_value = 'token'), \
|
||||
patch('facefusion.apis.stream_helper.session_manager.find_session_id', return_value = 'session-1'), \
|
||||
patch('facefusion.apis.stream_helper.session_context.set_session_id'), \
|
||||
patch('facefusion.apis.stream_helper.state_manager.get_item', return_value = 30), \
|
||||
patch('facefusion.apis.stream_helper.encode_video_loop'), \
|
||||
patch('facefusion.apis.stream_helper.encode_audio_loop') as mock_audio_loop, \
|
||||
patch('facefusion.apis.stream_helper.rtc_store'):
|
||||
asyncio.run(handle_video_stream(websocket))
|
||||
audio_queue = mock_audio_loop.call_args[0][1]
|
||||
assert create_hash(audio_queue.get_nowait()) == '6d72f0fc'
|
||||
Reference in New Issue
Block a user