libdatachannel-py 2025.1.0.dev8__tar.gz → 2025.1.0.dev9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {libdatachannel_py-2025.1.0.dev8/src/libdatachannel_py.egg-info → libdatachannel_py-2025.1.0.dev9}/PKG-INFO +2 -1
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/README.md +1 -0
- libdatachannel_py-2025.1.0.dev9/VERSION +1 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/pyproject.toml +7 -2
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/libdatachannel_ext.cpython-313-darwin.so +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9/src/libdatachannel_py.egg-info}/PKG-INFO +2 -1
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel_py.egg-info/SOURCES.txt +1 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_audio_codec.py +56 -2
- libdatachannel_py-2025.1.0.dev9/tests/test_prop_audio_codec.py +97 -0
- libdatachannel_py-2025.1.0.dev9/tests/test_video_codec.py +513 -0
- libdatachannel_py-2025.1.0.dev8/VERSION +0 -1
- libdatachannel_py-2025.1.0.dev8/tests/test_video_codec.py +0 -202
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/LICENSE +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/MANIFEST.in +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/buildbase.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/pypath.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/run.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/setup.cfg +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/setup.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/__init__.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/libdatachannel_ext.pyi +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/py.typed +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel_py.egg-info/dependency_links.txt +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel_py.egg-info/top_level.txt +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_candidate.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_configuration.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_description.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_frameinfo.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_h265nalunit.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_libyuv.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_mediahandler.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_message.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_nalunit.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_packetizationconfig.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_peerconnection.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_reliability.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_rtppacketizer.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_websocket.py +0 -0
- {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_websocketserver.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libdatachannel-py
|
|
3
|
-
Version: 2025.1.0.
|
|
3
|
+
Version: 2025.1.0.dev9
|
|
4
4
|
Summary: libdatachannel-py is a Python binding for libdatachannel.
|
|
5
5
|
Home-page: https://github.com/shiguredo/libdatachannel-py
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -39,6 +39,7 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
|
|
|
39
39
|
|
|
40
40
|
- [nanobind](https://github.com/wjakob/nanobind) を利用しています
|
|
41
41
|
- [Opus](https://opus-codec.org/) を利用した Opus ソフトウェアエンコード/デコードに対応しています
|
|
42
|
+
- [libaom](https://aomedia.googlesource.com/aom/) を利用した AV1 ソフトウェアエンコード/デコードに対応しています
|
|
42
43
|
- [OpenH264](https://www.openh264.org/) を利用した H.264 ソフトウェアエンコード/デコードに対応しています
|
|
43
44
|
- [Apple Video Toolbox](https://developer.apple.com/documentation/videotoolbox) を利用した H.264/H.265 ハードウェアエンコード/デコードに対応しています
|
|
44
45
|
|
|
@@ -23,6 +23,7 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
|
|
|
23
23
|
|
|
24
24
|
- [nanobind](https://github.com/wjakob/nanobind) を利用しています
|
|
25
25
|
- [Opus](https://opus-codec.org/) を利用した Opus ソフトウェアエンコード/デコードに対応しています
|
|
26
|
+
- [libaom](https://aomedia.googlesource.com/aom/) を利用した AV1 ソフトウェアエンコード/デコードに対応しています
|
|
26
27
|
- [OpenH264](https://www.openh264.org/) を利用した H.264 ソフトウェアエンコード/デコードに対応しています
|
|
27
28
|
- [Apple Video Toolbox](https://developer.apple.com/documentation/videotoolbox) を利用した H.264/H.265 ハードウェアエンコード/デコードに対応しています
|
|
28
29
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2025.1.0.dev9
|
|
@@ -18,19 +18,24 @@ Source = "https://github.com/shiguredo/libdatachannel-py"
|
|
|
18
18
|
Discord = "https://discord.gg/shiguredo"
|
|
19
19
|
|
|
20
20
|
[build-system]
|
|
21
|
-
requires = ["setuptools>=80.
|
|
21
|
+
requires = ["setuptools>=80.9", "wheel>=0.45.1"]
|
|
22
22
|
build-backend = "setuptools.build_meta"
|
|
23
23
|
|
|
24
24
|
[dependency-groups]
|
|
25
25
|
dev = [
|
|
26
26
|
"nanobind>=2.7",
|
|
27
27
|
"pytest>=8",
|
|
28
|
-
"setuptools>=80.
|
|
28
|
+
"setuptools>=80.9",
|
|
29
29
|
"build>=1.2.2.post1",
|
|
30
30
|
"wheel>=0.45.1",
|
|
31
31
|
"numpy>=2.2",
|
|
32
|
+
"ty",
|
|
33
|
+
"hypothesis>=6.132.0",
|
|
32
34
|
]
|
|
33
35
|
|
|
36
|
+
[tool.uv.workspace]
|
|
37
|
+
members = ["examples"]
|
|
38
|
+
|
|
34
39
|
[tool.ruff]
|
|
35
40
|
target-version = "py313"
|
|
36
41
|
line-length = 100
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libdatachannel-py
|
|
3
|
-
Version: 2025.1.0.
|
|
3
|
+
Version: 2025.1.0.dev9
|
|
4
4
|
Summary: libdatachannel-py is a Python binding for libdatachannel.
|
|
5
5
|
Home-page: https://github.com/shiguredo/libdatachannel-py
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -39,6 +39,7 @@ Please read <https://github.com/shiguredo/oss/blob/master/README.en.md> before u
|
|
|
39
39
|
|
|
40
40
|
- [nanobind](https://github.com/wjakob/nanobind) を利用しています
|
|
41
41
|
- [Opus](https://opus-codec.org/) を利用した Opus ソフトウェアエンコード/デコードに対応しています
|
|
42
|
+
- [libaom](https://aomedia.googlesource.com/aom/) を利用した AV1 ソフトウェアエンコード/デコードに対応しています
|
|
42
43
|
- [OpenH264](https://www.openh264.org/) を利用した H.264 ソフトウェアエンコード/デコードに対応しています
|
|
43
44
|
- [Apple Video Toolbox](https://developer.apple.com/documentation/videotoolbox) を利用した H.264/H.265 ハードウェアエンコード/デコードに対応しています
|
|
44
45
|
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_audio_codec.py
RENAMED
|
@@ -4,9 +4,11 @@ import numpy as np
|
|
|
4
4
|
|
|
5
5
|
from libdatachannel.codec import (
|
|
6
6
|
AudioCodecType,
|
|
7
|
+
AudioDecoder,
|
|
7
8
|
AudioEncoder,
|
|
8
9
|
AudioFrame,
|
|
9
10
|
EncodedAudio,
|
|
11
|
+
create_opus_audio_decoder,
|
|
10
12
|
create_opus_audio_encoder,
|
|
11
13
|
)
|
|
12
14
|
|
|
@@ -34,14 +36,14 @@ def test_audio_encoder_encode():
|
|
|
34
36
|
frame = AudioFrame()
|
|
35
37
|
frame.sample_rate = 48000
|
|
36
38
|
frame.timestamp = timedelta(milliseconds=1000)
|
|
37
|
-
# 48000 Hz * 20 ms = 960
|
|
39
|
+
# 48000 Hz * 20 ms = 960 サンプル
|
|
38
40
|
frame.pcm = np.zeros((960, 2), dtype=np.float32)
|
|
39
41
|
|
|
40
42
|
encoder.encode(frame)
|
|
41
43
|
|
|
42
44
|
assert callback_timestamps == [timedelta(milliseconds=1000)]
|
|
43
45
|
|
|
44
|
-
# 10ms
|
|
46
|
+
# 10ms -> 20ms でエンコード
|
|
45
47
|
frame = AudioFrame()
|
|
46
48
|
frame.sample_rate = 48000
|
|
47
49
|
frame.timestamp = timedelta(milliseconds=1000)
|
|
@@ -70,3 +72,55 @@ def test_audio_encoder_encode():
|
|
|
70
72
|
]
|
|
71
73
|
|
|
72
74
|
encoder.release()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_audio_encoder_decoder():
|
|
78
|
+
"""エンコード -> デコードのテスト"""
|
|
79
|
+
encoder = create_opus_audio_encoder()
|
|
80
|
+
encoder_settings = AudioEncoder.Settings()
|
|
81
|
+
encoder_settings.codec_type = AudioCodecType.OPUS
|
|
82
|
+
encoder_settings.sample_rate = 48000
|
|
83
|
+
encoder_settings.channels = 2
|
|
84
|
+
encoder_settings.bitrate = 64000
|
|
85
|
+
encoder_settings.frame_duration_ms = 20
|
|
86
|
+
assert encoder.init(encoder_settings) is True
|
|
87
|
+
|
|
88
|
+
decoder = create_opus_audio_decoder()
|
|
89
|
+
decoder_settings = AudioDecoder.Settings()
|
|
90
|
+
decoder_settings.codec_type = AudioCodecType.OPUS
|
|
91
|
+
decoder_settings.sample_rate = 48000
|
|
92
|
+
decoder_settings.channels = 2
|
|
93
|
+
assert decoder.init(decoder_settings) is True
|
|
94
|
+
|
|
95
|
+
encoded_data = []
|
|
96
|
+
decoded_frames = []
|
|
97
|
+
|
|
98
|
+
def on_encoded(encoded: EncodedAudio):
|
|
99
|
+
encoded_data.append(encoded)
|
|
100
|
+
|
|
101
|
+
def on_decoded(frame: AudioFrame):
|
|
102
|
+
decoded_frames.append(frame)
|
|
103
|
+
|
|
104
|
+
encoder.set_on_encode(on_encoded)
|
|
105
|
+
decoder.set_on_decode(on_decoded)
|
|
106
|
+
|
|
107
|
+
# 無音データでテスト
|
|
108
|
+
frame = AudioFrame()
|
|
109
|
+
frame.sample_rate = 48000
|
|
110
|
+
frame.timestamp = timedelta(milliseconds=1000)
|
|
111
|
+
frame.pcm = np.zeros((960, 2), dtype=np.float32)
|
|
112
|
+
|
|
113
|
+
encoder.encode(frame)
|
|
114
|
+
assert len(encoded_data) == 1
|
|
115
|
+
|
|
116
|
+
decoder.decode(encoded_data[0])
|
|
117
|
+
assert len(decoded_frames) == 1
|
|
118
|
+
|
|
119
|
+
decoded_frame = decoded_frames[0]
|
|
120
|
+
assert decoded_frame.sample_rate == 48000
|
|
121
|
+
assert decoded_frame.channels() == 2
|
|
122
|
+
assert decoded_frame.samples() == 960
|
|
123
|
+
assert decoded_frame.timestamp == timedelta(milliseconds=1000)
|
|
124
|
+
|
|
125
|
+
encoder.release()
|
|
126
|
+
decoder.release()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from hypothesis import given, settings
|
|
5
|
+
from hypothesis import strategies as st
|
|
6
|
+
|
|
7
|
+
from libdatachannel.codec import (
|
|
8
|
+
AudioCodecType,
|
|
9
|
+
AudioDecoder,
|
|
10
|
+
AudioEncoder,
|
|
11
|
+
AudioFrame,
|
|
12
|
+
EncodedAudio,
|
|
13
|
+
create_opus_audio_decoder,
|
|
14
|
+
create_opus_audio_encoder,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@settings(max_examples=1000)
|
|
19
|
+
@given(
|
|
20
|
+
samples=st.integers(min_value=0, max_value=96000),
|
|
21
|
+
# https://www.rfc-editor.org/rfc/rfc6716#section-2.1.1
|
|
22
|
+
# Opus supports all bitrates from 6 kbit/s to 510 kbit/s.
|
|
23
|
+
bitrate=st.integers(min_value=6000, max_value=510000),
|
|
24
|
+
# https://www.rfc-editor.org/rfc/rfc6716#section-2.1.4
|
|
25
|
+
# Opus can encode frames of 2.5, 5, 10, 20, 40, or 60 ms. It can also combine multiple frames into packets of up to 120 ms.
|
|
26
|
+
# TODO: 2.5ms は現時点では対応しない
|
|
27
|
+
# frame_duration_ms=st.sampled_from([2.5, 5, 10, 20, 40, 60, 120]),
|
|
28
|
+
frame_duration_ms=st.sampled_from([5, 10, 20, 40, 60, 120]),
|
|
29
|
+
)
|
|
30
|
+
def test_prop_audio_encode_decode(samples, bitrate, frame_duration_ms):
|
|
31
|
+
"""PBT を利用したエンコーダー・デコーダーのテスト"""
|
|
32
|
+
encoder = create_opus_audio_encoder()
|
|
33
|
+
encoder_settings = AudioEncoder.Settings()
|
|
34
|
+
encoder_settings.codec_type = AudioCodecType.OPUS
|
|
35
|
+
encoder_settings.sample_rate = 48000
|
|
36
|
+
encoder_settings.channels = 2
|
|
37
|
+
encoder_settings.bitrate = bitrate
|
|
38
|
+
encoder_settings.frame_duration_ms = frame_duration_ms
|
|
39
|
+
assert encoder.init(encoder_settings) is True
|
|
40
|
+
|
|
41
|
+
decoder = create_opus_audio_decoder()
|
|
42
|
+
decoder_settings = AudioDecoder.Settings()
|
|
43
|
+
decoder_settings.codec_type = AudioCodecType.OPUS
|
|
44
|
+
decoder_settings.sample_rate = 48000
|
|
45
|
+
decoder_settings.channels = 2
|
|
46
|
+
assert decoder.init(decoder_settings) is True
|
|
47
|
+
|
|
48
|
+
encoded_data = []
|
|
49
|
+
decoded_frames = []
|
|
50
|
+
|
|
51
|
+
def on_encoded(encoded: EncodedAudio):
|
|
52
|
+
assert isinstance(encoded.data, np.ndarray)
|
|
53
|
+
assert len(encoded.data) > 0
|
|
54
|
+
encoded_data.append(encoded)
|
|
55
|
+
|
|
56
|
+
def on_decoded(frame: AudioFrame):
|
|
57
|
+
decoded_frames.append(frame)
|
|
58
|
+
|
|
59
|
+
encoder.set_on_encode(on_encoded)
|
|
60
|
+
decoder.set_on_decode(on_decoded)
|
|
61
|
+
|
|
62
|
+
frame = AudioFrame()
|
|
63
|
+
frame.sample_rate = 48000
|
|
64
|
+
frame.timestamp = timedelta(milliseconds=1000)
|
|
65
|
+
|
|
66
|
+
# 時間軸を生成 (サンプル数 / サンプリングレート = 秒)
|
|
67
|
+
t = np.arange(samples) / 48000
|
|
68
|
+
# 440Hz のサイン波を生成 (振幅 0.1)
|
|
69
|
+
audio_signal = np.sin(2 * np.pi * 440 * t) * 0.1
|
|
70
|
+
frame.pcm = np.column_stack((audio_signal, audio_signal)).astype(np.float32)
|
|
71
|
+
|
|
72
|
+
# エンコード実行
|
|
73
|
+
encoder.encode(frame)
|
|
74
|
+
|
|
75
|
+
# frame_duration_ms に基づいて期待されるフレーム数を計算
|
|
76
|
+
samples_per_frame = int(48000 * frame_duration_ms / 1000)
|
|
77
|
+
expected_frames = samples // samples_per_frame
|
|
78
|
+
assert len(encoded_data) == expected_frames
|
|
79
|
+
|
|
80
|
+
# デコード実行
|
|
81
|
+
for encoded in encoded_data:
|
|
82
|
+
decoder.decode(encoded)
|
|
83
|
+
|
|
84
|
+
# デコードされたフレーム数の確認
|
|
85
|
+
assert len(decoded_frames) == expected_frames
|
|
86
|
+
|
|
87
|
+
# デコードされたフレームのプロパティを確認
|
|
88
|
+
for i, decoded_frame in enumerate(decoded_frames):
|
|
89
|
+
assert decoded_frame.sample_rate == 48000
|
|
90
|
+
assert decoded_frame.channels() == 2
|
|
91
|
+
assert decoded_frame.samples() == samples_per_frame
|
|
92
|
+
# タイムスタンプの確認
|
|
93
|
+
expected_timestamp = timedelta(milliseconds=1000 + i * frame_duration_ms)
|
|
94
|
+
assert decoded_frame.timestamp == expected_timestamp
|
|
95
|
+
|
|
96
|
+
encoder.release()
|
|
97
|
+
decoder.release()
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from libdatachannel import (
|
|
9
|
+
NalUnit,
|
|
10
|
+
NalUnitFragmentA,
|
|
11
|
+
)
|
|
12
|
+
from libdatachannel.codec import (
|
|
13
|
+
EncodedImage,
|
|
14
|
+
ImageFormat,
|
|
15
|
+
VideoCodecType,
|
|
16
|
+
VideoDecoder,
|
|
17
|
+
VideoEncoder,
|
|
18
|
+
VideoFrame,
|
|
19
|
+
VideoFrameBufferBGR888,
|
|
20
|
+
VideoFrameBufferI420,
|
|
21
|
+
VideoFrameBufferNV12,
|
|
22
|
+
create_aom_video_decoder,
|
|
23
|
+
create_aom_video_encoder,
|
|
24
|
+
create_openh264_video_decoder,
|
|
25
|
+
create_openh264_video_encoder,
|
|
26
|
+
create_videotoolbox_video_encoder,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_video_frame_buffer():
|
|
31
|
+
# I420
|
|
32
|
+
i420_buffer = VideoFrameBufferI420.create(640, 480)
|
|
33
|
+
assert i420_buffer.width() == 640
|
|
34
|
+
assert i420_buffer.height() == 480
|
|
35
|
+
assert i420_buffer.stride_y() == 640
|
|
36
|
+
assert i420_buffer.stride_u() == 320
|
|
37
|
+
assert i420_buffer.stride_v() == 320
|
|
38
|
+
assert isinstance(i420_buffer.y, np.ndarray)
|
|
39
|
+
assert isinstance(i420_buffer.u, np.ndarray)
|
|
40
|
+
assert isinstance(i420_buffer.v, np.ndarray)
|
|
41
|
+
i420_buffer.y[0] = 255
|
|
42
|
+
i420_buffer.y = np.zeros((100, 100), dtype=np.uint8)
|
|
43
|
+
assert i420_buffer.width() == 100
|
|
44
|
+
|
|
45
|
+
# NV12
|
|
46
|
+
nv12_buffer = VideoFrameBufferNV12.create(640, 480)
|
|
47
|
+
assert nv12_buffer.width() == 640
|
|
48
|
+
assert nv12_buffer.height() == 480
|
|
49
|
+
assert nv12_buffer.stride_y() == 640
|
|
50
|
+
assert nv12_buffer.stride_uv() == 640
|
|
51
|
+
assert isinstance(nv12_buffer.y, np.ndarray)
|
|
52
|
+
assert isinstance(nv12_buffer.uv, np.ndarray)
|
|
53
|
+
|
|
54
|
+
# BGR888
|
|
55
|
+
bgr888_buffer = VideoFrameBufferBGR888.create(640, 480)
|
|
56
|
+
assert bgr888_buffer.width() == 640
|
|
57
|
+
assert bgr888_buffer.height() == 480
|
|
58
|
+
assert bgr888_buffer.stride() == 640 * 3
|
|
59
|
+
assert isinstance(bgr888_buffer.bgr, np.ndarray)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_video_frame_properties():
|
|
63
|
+
frame = VideoFrame()
|
|
64
|
+
|
|
65
|
+
# format による width()/height() の分岐
|
|
66
|
+
frame.i420_buffer = VideoFrameBufferI420.create(640, 360)
|
|
67
|
+
frame.nv12_buffer = VideoFrameBufferNV12.create(1280, 720)
|
|
68
|
+
frame.bgr888_buffer = VideoFrameBufferBGR888.create(1920, 1080)
|
|
69
|
+
|
|
70
|
+
frame.format = ImageFormat.I420
|
|
71
|
+
assert frame.width() == 640
|
|
72
|
+
assert frame.height() == 360
|
|
73
|
+
|
|
74
|
+
frame.format = ImageFormat.NV12
|
|
75
|
+
assert frame.width() == 1280
|
|
76
|
+
assert frame.height() == 720
|
|
77
|
+
|
|
78
|
+
frame.format = ImageFormat.BGR888
|
|
79
|
+
assert frame.width() == 1920
|
|
80
|
+
assert frame.height() == 1080
|
|
81
|
+
|
|
82
|
+
# timestamp, rid
|
|
83
|
+
ts = timedelta(microseconds=1234567)
|
|
84
|
+
frame.timestamp = ts
|
|
85
|
+
assert frame.timestamp == ts
|
|
86
|
+
|
|
87
|
+
frame.rid = "abc"
|
|
88
|
+
assert frame.rid == "abc"
|
|
89
|
+
|
|
90
|
+
# base_width, base_height
|
|
91
|
+
frame.base_width = 999
|
|
92
|
+
frame.base_height = 888
|
|
93
|
+
assert frame.base_width == 999
|
|
94
|
+
assert frame.base_height == 888
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_encoded_image_properties():
|
|
98
|
+
img = EncodedImage()
|
|
99
|
+
|
|
100
|
+
img.data = np.array([], dtype=np.uint8)
|
|
101
|
+
assert img.data.size == 0
|
|
102
|
+
img.data = np.array([1, 2, 3], dtype=np.uint8)
|
|
103
|
+
assert img.data.size == 3
|
|
104
|
+
|
|
105
|
+
ts = timedelta(microseconds=555000)
|
|
106
|
+
img.timestamp = ts
|
|
107
|
+
assert img.timestamp == ts
|
|
108
|
+
|
|
109
|
+
img.rid = "r0"
|
|
110
|
+
assert img.rid == "r0"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_openh264():
|
|
114
|
+
openh264 = os.environ.get("OPENH264_PATH")
|
|
115
|
+
assert openh264 is not None
|
|
116
|
+
encoder = create_openh264_video_encoder(openh264)
|
|
117
|
+
settings = VideoEncoder.Settings()
|
|
118
|
+
settings.codec_type = VideoCodecType.H264
|
|
119
|
+
settings.width = 640
|
|
120
|
+
settings.height = 480
|
|
121
|
+
settings.bitrate = 1000000
|
|
122
|
+
success = encoder.init(settings)
|
|
123
|
+
assert success
|
|
124
|
+
frame = VideoFrame()
|
|
125
|
+
frame.i420_buffer = VideoFrameBufferI420.create(640, 360)
|
|
126
|
+
frame.format = ImageFormat.I420
|
|
127
|
+
frame.base_width = 640
|
|
128
|
+
frame.base_height = 480
|
|
129
|
+
frame.timestamp = timedelta(microseconds=1234567)
|
|
130
|
+
|
|
131
|
+
on_encode_called = False
|
|
132
|
+
|
|
133
|
+
def on_encode(encoded_image):
|
|
134
|
+
nonlocal on_encode_called
|
|
135
|
+
assert encoded_image.data.size > 0
|
|
136
|
+
assert encoded_image.timestamp == frame.timestamp
|
|
137
|
+
assert encoded_image.rid == frame.rid
|
|
138
|
+
on_encode_called = True
|
|
139
|
+
|
|
140
|
+
encoder.set_on_encode(on_encode)
|
|
141
|
+
encoder.encode(frame)
|
|
142
|
+
encoder.release()
|
|
143
|
+
assert on_encode_called
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.skipif(
|
|
147
|
+
os.environ.get("ENABLE_VIDEOTOOLBOX") is None, reason="macOS の場合だけ実行する"
|
|
148
|
+
)
|
|
149
|
+
def test_videotoolbox():
|
|
150
|
+
encoder = create_videotoolbox_video_encoder()
|
|
151
|
+
settings = VideoEncoder.Settings()
|
|
152
|
+
settings.codec_type = VideoCodecType.H264
|
|
153
|
+
settings.width = 640
|
|
154
|
+
settings.height = 480
|
|
155
|
+
settings.bitrate = 1000000
|
|
156
|
+
success = encoder.init(settings)
|
|
157
|
+
assert success
|
|
158
|
+
frame = VideoFrame()
|
|
159
|
+
frame.nv12_buffer = VideoFrameBufferNV12.create(640, 360)
|
|
160
|
+
frame.format = ImageFormat.NV12
|
|
161
|
+
frame.base_width = 640
|
|
162
|
+
frame.base_height = 480
|
|
163
|
+
frame.timestamp = timedelta(microseconds=1234567)
|
|
164
|
+
|
|
165
|
+
on_encode_called = False
|
|
166
|
+
|
|
167
|
+
def on_encode(encoded_image):
|
|
168
|
+
nonlocal on_encode_called
|
|
169
|
+
assert encoded_image.data.size > 0
|
|
170
|
+
assert encoded_image.timestamp == frame.timestamp
|
|
171
|
+
assert encoded_image.rid == frame.rid
|
|
172
|
+
on_encode_called = True
|
|
173
|
+
|
|
174
|
+
encoder.set_on_encode(on_encode)
|
|
175
|
+
encoder.encode(frame)
|
|
176
|
+
time.sleep(1)
|
|
177
|
+
encoder.release()
|
|
178
|
+
assert on_encode_called
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_aom():
|
|
182
|
+
encoder = create_aom_video_encoder()
|
|
183
|
+
settings = VideoEncoder.Settings()
|
|
184
|
+
settings.codec_type = VideoCodecType.H264
|
|
185
|
+
settings.width = 640
|
|
186
|
+
settings.height = 480
|
|
187
|
+
settings.bitrate = 1000000
|
|
188
|
+
success = encoder.init(settings)
|
|
189
|
+
assert success
|
|
190
|
+
frame = VideoFrame()
|
|
191
|
+
frame.i420_buffer = VideoFrameBufferI420.create(640, 360)
|
|
192
|
+
frame.format = ImageFormat.I420
|
|
193
|
+
frame.base_width = 640
|
|
194
|
+
frame.base_height = 480
|
|
195
|
+
frame.timestamp = timedelta(microseconds=1234567)
|
|
196
|
+
|
|
197
|
+
on_encode_called = False
|
|
198
|
+
|
|
199
|
+
def on_encode(encoded_image):
|
|
200
|
+
nonlocal on_encode_called
|
|
201
|
+
assert encoded_image.data.size > 0
|
|
202
|
+
assert encoded_image.timestamp == frame.timestamp
|
|
203
|
+
assert encoded_image.rid == frame.rid
|
|
204
|
+
on_encode_called = True
|
|
205
|
+
|
|
206
|
+
encoder.set_on_encode(on_encode)
|
|
207
|
+
encoder.encode(frame)
|
|
208
|
+
encoder.release()
|
|
209
|
+
assert on_encode_called
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_openh264_encode_decode():
|
|
213
|
+
"""OpenH264を使用してnumpyで生成した映像をエンコード・デコードするテスト"""
|
|
214
|
+
openh264 = os.environ.get("OPENH264_PATH")
|
|
215
|
+
assert openh264 is not None
|
|
216
|
+
|
|
217
|
+
# エンコーダーの初期化
|
|
218
|
+
encoder = create_openh264_video_encoder(openh264)
|
|
219
|
+
settings = VideoEncoder.Settings()
|
|
220
|
+
settings.codec_type = VideoCodecType.H264
|
|
221
|
+
settings.width = 320
|
|
222
|
+
settings.height = 240
|
|
223
|
+
settings.bitrate = 500000
|
|
224
|
+
settings.fps = 30
|
|
225
|
+
success = encoder.init(settings)
|
|
226
|
+
assert success
|
|
227
|
+
|
|
228
|
+
# テスト用の映像フレームを生成(グラデーションパターン)
|
|
229
|
+
width, height = 320, 240
|
|
230
|
+
i420_buffer = VideoFrameBufferI420.create(width, height)
|
|
231
|
+
|
|
232
|
+
# Y平面:グラデーション(左から右へ明るくなる)
|
|
233
|
+
y_gradient = np.linspace(16, 235, width, dtype=np.uint8)
|
|
234
|
+
y_plane = y_gradient[np.newaxis, :].repeat(height, axis=0)
|
|
235
|
+
i420_buffer.y[:, :] = y_plane
|
|
236
|
+
|
|
237
|
+
# U,V平面:単色(グレー)
|
|
238
|
+
i420_buffer.u[:, :] = 128
|
|
239
|
+
i420_buffer.v[:, :] = 128
|
|
240
|
+
|
|
241
|
+
frame = VideoFrame()
|
|
242
|
+
frame.i420_buffer = i420_buffer
|
|
243
|
+
frame.format = ImageFormat.I420
|
|
244
|
+
frame.base_width = width
|
|
245
|
+
frame.base_height = height
|
|
246
|
+
frame.timestamp = timedelta(microseconds=1000)
|
|
247
|
+
|
|
248
|
+
encoded_frames = []
|
|
249
|
+
|
|
250
|
+
def on_encode(encoded_image):
|
|
251
|
+
encoded_frames.append(encoded_image)
|
|
252
|
+
assert encoded_image.data.size > 0
|
|
253
|
+
|
|
254
|
+
encoder.set_on_encode(on_encode)
|
|
255
|
+
encoder.force_intra_next_frame() # キーフレームを生成
|
|
256
|
+
encoder.encode(frame)
|
|
257
|
+
|
|
258
|
+
# 2フレーム目も送信
|
|
259
|
+
frame.timestamp = timedelta(microseconds=1000 + 33333) # 30fps
|
|
260
|
+
encoder.encode(frame)
|
|
261
|
+
encoder.release()
|
|
262
|
+
|
|
263
|
+
assert len(encoded_frames) == 2
|
|
264
|
+
print(f"Encoded {len(encoded_frames)} frames")
|
|
265
|
+
|
|
266
|
+
# デコーダーでデコード
|
|
267
|
+
decoder = create_openh264_video_decoder(openh264)
|
|
268
|
+
decoder_settings = VideoDecoder.Settings()
|
|
269
|
+
decoder_settings.codec_type = VideoCodecType.H264
|
|
270
|
+
success = decoder.init(decoder_settings)
|
|
271
|
+
assert success
|
|
272
|
+
|
|
273
|
+
decoded_frames = []
|
|
274
|
+
|
|
275
|
+
def on_decode(video_frame):
|
|
276
|
+
nonlocal decoded_frames
|
|
277
|
+
decoded_frames.append(video_frame)
|
|
278
|
+
print(f"Decoded frame: {video_frame.width()}x{video_frame.height()}")
|
|
279
|
+
|
|
280
|
+
decoder.set_on_decode(on_decode)
|
|
281
|
+
|
|
282
|
+
# すべてのエンコード済みフレームをデコード
|
|
283
|
+
for i, encoded in enumerate(encoded_frames):
|
|
284
|
+
encoded_image = EncodedImage()
|
|
285
|
+
encoded_image.data = encoded.data
|
|
286
|
+
encoded_image.timestamp = encoded.timestamp
|
|
287
|
+
print(f"Decoding frame {i + 1}: {encoded_image.data.size} bytes...")
|
|
288
|
+
decoder.decode(encoded_image)
|
|
289
|
+
|
|
290
|
+
decoder.release()
|
|
291
|
+
|
|
292
|
+
assert len(decoded_frames) == 2
|
|
293
|
+
assert decoded_frames[0].width() == width
|
|
294
|
+
assert decoded_frames[0].height() == height
|
|
295
|
+
assert decoded_frames[0].format == ImageFormat.I420
|
|
296
|
+
assert decoded_frames[0].timestamp == timedelta(microseconds=1000)
|
|
297
|
+
assert decoded_frames[1].width() == width
|
|
298
|
+
assert decoded_frames[1].height() == height
|
|
299
|
+
assert decoded_frames[1].format == ImageFormat.I420
|
|
300
|
+
assert decoded_frames[1].timestamp == timedelta(microseconds=1000 + 33333)
|
|
301
|
+
|
|
302
|
+
# デコードされたフレームの内容を簡単に検証
|
|
303
|
+
# Y平面の最初と最後の値がグラデーションになっているか確認
|
|
304
|
+
decoded_y = decoded_frames[0].i420_buffer.y.reshape(height, width)
|
|
305
|
+
assert decoded_y[0, 0] < decoded_y[0, -1] # 左端より右端の方が明るい
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def test_openh264_with_nal_units():
|
|
309
|
+
"""OpenH264とNALユニット解析を使用したテスト"""
|
|
310
|
+
openh264 = os.environ.get("OPENH264_PATH")
|
|
311
|
+
assert openh264 is not None
|
|
312
|
+
|
|
313
|
+
# エンコーダーの初期化
|
|
314
|
+
encoder = create_openh264_video_encoder(openh264)
|
|
315
|
+
settings = VideoEncoder.Settings()
|
|
316
|
+
settings.codec_type = VideoCodecType.H264
|
|
317
|
+
settings.width = 320
|
|
318
|
+
settings.height = 240
|
|
319
|
+
settings.bitrate = 500000
|
|
320
|
+
settings.fps = 30
|
|
321
|
+
success = encoder.init(settings)
|
|
322
|
+
assert success
|
|
323
|
+
|
|
324
|
+
# テスト用の映像フレームを生成(グラデーションパターン)
|
|
325
|
+
width, height = 320, 240
|
|
326
|
+
i420_buffer = VideoFrameBufferI420.create(width, height)
|
|
327
|
+
|
|
328
|
+
# Y平面:グラデーション(左から右へ明るくなる)
|
|
329
|
+
y_gradient = np.linspace(16, 235, width, dtype=np.uint8)
|
|
330
|
+
y_plane = y_gradient[np.newaxis, :].repeat(height, axis=0)
|
|
331
|
+
i420_buffer.y[:, :] = y_plane
|
|
332
|
+
|
|
333
|
+
# U,V平面:単色(グレー)
|
|
334
|
+
i420_buffer.u[:, :] = 128
|
|
335
|
+
i420_buffer.v[:, :] = 128
|
|
336
|
+
|
|
337
|
+
frame = VideoFrame()
|
|
338
|
+
frame.i420_buffer = i420_buffer
|
|
339
|
+
frame.format = ImageFormat.I420
|
|
340
|
+
frame.base_width = width
|
|
341
|
+
frame.base_height = height
|
|
342
|
+
frame.timestamp = timedelta(microseconds=1000)
|
|
343
|
+
|
|
344
|
+
encoded_frames = []
|
|
345
|
+
|
|
346
|
+
def on_encode(encoded_image):
|
|
347
|
+
encoded_frames.append(encoded_image)
|
|
348
|
+
|
|
349
|
+
encoder.set_on_encode(on_encode)
|
|
350
|
+
encoder.force_intra_next_frame() # キーフレームを生成
|
|
351
|
+
encoder.encode(frame)
|
|
352
|
+
|
|
353
|
+
# 2フレーム目も送信
|
|
354
|
+
frame.timestamp = timedelta(microseconds=1000 + 33333)
|
|
355
|
+
encoder.encode(frame)
|
|
356
|
+
encoder.release()
|
|
357
|
+
|
|
358
|
+
assert len(encoded_frames) == 2
|
|
359
|
+
print(f"Encoded {len(encoded_frames)} frames")
|
|
360
|
+
|
|
361
|
+
# 最初のフレームのNALユニットを解析
|
|
362
|
+
first_encoded = encoded_frames[0].data
|
|
363
|
+
print(f"First frame size: {first_encoded.size} bytes")
|
|
364
|
+
|
|
365
|
+
# NALユニットを分割して作成
|
|
366
|
+
nal_units_byte = bytes(first_encoded).split(b"\x00\x00\x00\x01")[1:]
|
|
367
|
+
nal_units = list(map(lambda b: NalUnit(b), nal_units_byte))
|
|
368
|
+
|
|
369
|
+
# NALユニットのヘッダー情報を検証
|
|
370
|
+
assert len(nal_units) == 3
|
|
371
|
+
assert nal_units[0].unit_type() == 7 # SPS
|
|
372
|
+
assert nal_units[1].unit_type() == 8 # PPS
|
|
373
|
+
assert nal_units[2].unit_type() == 5 # IDR
|
|
374
|
+
|
|
375
|
+
# 複数のNALユニットに分割(フラグメント化)のテスト
|
|
376
|
+
fragments = NalUnitFragmentA.fragments_from(nal_units[2], max_fragment_size=100)
|
|
377
|
+
assert len(fragments) == 5
|
|
378
|
+
print(f"Generated {len(fragments)} fragments")
|
|
379
|
+
|
|
380
|
+
# デコーダーでデコード
|
|
381
|
+
decoder = create_openh264_video_decoder(openh264)
|
|
382
|
+
decoder_settings = VideoDecoder.Settings()
|
|
383
|
+
decoder_settings.codec_type = VideoCodecType.H264
|
|
384
|
+
success = decoder.init(decoder_settings)
|
|
385
|
+
assert success
|
|
386
|
+
|
|
387
|
+
decoded_frames = []
|
|
388
|
+
|
|
389
|
+
def on_decode(video_frame):
|
|
390
|
+
nonlocal decoded_frames
|
|
391
|
+
decoded_frames.append(video_frame)
|
|
392
|
+
print(f"Decoded frame: {video_frame.width()}x{video_frame.height()}")
|
|
393
|
+
|
|
394
|
+
decoder.set_on_decode(on_decode)
|
|
395
|
+
|
|
396
|
+
# すべてのエンコード済みフレームをデコード
|
|
397
|
+
for i, encoded in enumerate(encoded_frames):
|
|
398
|
+
encoded_image = EncodedImage()
|
|
399
|
+
encoded_image.data = encoded.data
|
|
400
|
+
encoded_image.timestamp = encoded.timestamp
|
|
401
|
+
print(f"Decoding frame {i + 1}: {encoded_image.data.size} bytes...")
|
|
402
|
+
decoder.decode(encoded_image)
|
|
403
|
+
|
|
404
|
+
decoder.release()
|
|
405
|
+
|
|
406
|
+
# デコード結果の検証
|
|
407
|
+
assert len(decoded_frames) == 2
|
|
408
|
+
assert decoded_frames[0].width() == width
|
|
409
|
+
assert decoded_frames[0].height() == height
|
|
410
|
+
assert decoded_frames[0].timestamp == timedelta(microseconds=1000)
|
|
411
|
+
assert decoded_frames[1].width() == width
|
|
412
|
+
assert decoded_frames[1].height() == height
|
|
413
|
+
assert decoded_frames[1].timestamp == timedelta(microseconds=1000 + 33333)
|
|
414
|
+
|
|
415
|
+
# デコードされたフレームの内容を簡単に検証
|
|
416
|
+
decoded_y = decoded_frames[0].i420_buffer.y.reshape(height, width)
|
|
417
|
+
assert decoded_y[0, 0] < decoded_y[0, -1] # 左端より右端の方が明るい
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_aom_encode_decode():
|
|
421
|
+
"""AOMを使用してnumpyで生成した映像をエンコード・デコードするテスト"""
|
|
422
|
+
# エンコーダーの初期化
|
|
423
|
+
encoder = create_aom_video_encoder()
|
|
424
|
+
settings = VideoEncoder.Settings()
|
|
425
|
+
settings.codec_type = VideoCodecType.AV1
|
|
426
|
+
settings.width = 320
|
|
427
|
+
settings.height = 240
|
|
428
|
+
settings.bitrate = 500000
|
|
429
|
+
settings.fps = 30
|
|
430
|
+
success = encoder.init(settings)
|
|
431
|
+
assert success
|
|
432
|
+
|
|
433
|
+
# テスト用の映像フレームを生成(グラデーションパターン)
|
|
434
|
+
width, height = 320, 240
|
|
435
|
+
i420_buffer = VideoFrameBufferI420.create(width, height)
|
|
436
|
+
|
|
437
|
+
# Y平面:グラデーション(左から右へ明るくなる)
|
|
438
|
+
y_gradient = np.linspace(16, 235, width, dtype=np.uint8)
|
|
439
|
+
y_plane = y_gradient[np.newaxis, :].repeat(height, axis=0)
|
|
440
|
+
i420_buffer.y[:, :] = y_plane
|
|
441
|
+
|
|
442
|
+
# U,V平面:単色(グレー)
|
|
443
|
+
i420_buffer.u[:, :] = 128
|
|
444
|
+
i420_buffer.v[:, :] = 128
|
|
445
|
+
|
|
446
|
+
frame = VideoFrame()
|
|
447
|
+
frame.i420_buffer = i420_buffer
|
|
448
|
+
frame.format = ImageFormat.I420
|
|
449
|
+
frame.base_width = width
|
|
450
|
+
frame.base_height = height
|
|
451
|
+
frame.timestamp = timedelta(microseconds=1000)
|
|
452
|
+
|
|
453
|
+
encoded_frames = []
|
|
454
|
+
|
|
455
|
+
def on_encode(encoded_image):
|
|
456
|
+
encoded_frames.append(encoded_image)
|
|
457
|
+
assert encoded_image.data.size > 0
|
|
458
|
+
|
|
459
|
+
encoder.set_on_encode(on_encode)
|
|
460
|
+
encoder.force_intra_next_frame() # キーフレームを生成
|
|
461
|
+
encoder.encode(frame)
|
|
462
|
+
|
|
463
|
+
# 2フレーム目も送信
|
|
464
|
+
frame.timestamp = timedelta(microseconds=1000 + 33333) # 30fps
|
|
465
|
+
encoder.encode(frame)
|
|
466
|
+
encoder.release()
|
|
467
|
+
|
|
468
|
+
assert len(encoded_frames) == 2
|
|
469
|
+
print(f"Encoded {len(encoded_frames)} frames")
|
|
470
|
+
|
|
471
|
+
assert encoded_frames[0].timestamp == timedelta(microseconds=1000)
|
|
472
|
+
assert encoded_frames[1].timestamp == timedelta(microseconds=1000 + 33333)
|
|
473
|
+
|
|
474
|
+
# デコーダーでデコード
|
|
475
|
+
decoder = create_aom_video_decoder()
|
|
476
|
+
decoder_settings = VideoDecoder.Settings()
|
|
477
|
+
decoder_settings.codec_type = VideoCodecType.AV1
|
|
478
|
+
success = decoder.init(decoder_settings)
|
|
479
|
+
assert success
|
|
480
|
+
|
|
481
|
+
decoded_frames = []
|
|
482
|
+
|
|
483
|
+
def on_decode(video_frame):
|
|
484
|
+
nonlocal decoded_frames
|
|
485
|
+
decoded_frames.append(video_frame)
|
|
486
|
+
print(f"Decoded frame: {video_frame.width()}x{video_frame.height()}")
|
|
487
|
+
|
|
488
|
+
decoder.set_on_decode(on_decode)
|
|
489
|
+
|
|
490
|
+
# すべてのエンコード済みフレームをデコード
|
|
491
|
+
for i, encoded in enumerate(encoded_frames):
|
|
492
|
+
encoded_image = EncodedImage()
|
|
493
|
+
encoded_image.data = encoded.data
|
|
494
|
+
encoded_image.timestamp = encoded.timestamp
|
|
495
|
+
print(f"Decoding frame {i + 1}: {encoded_image.data.size} bytes...")
|
|
496
|
+
decoder.decode(encoded_image)
|
|
497
|
+
|
|
498
|
+
decoder.release()
|
|
499
|
+
|
|
500
|
+
assert len(decoded_frames) == 2
|
|
501
|
+
assert decoded_frames[0].width() == width
|
|
502
|
+
assert decoded_frames[0].height() == height
|
|
503
|
+
assert decoded_frames[0].format == ImageFormat.I420
|
|
504
|
+
assert decoded_frames[0].timestamp == timedelta(microseconds=1000)
|
|
505
|
+
assert decoded_frames[1].width() == width
|
|
506
|
+
assert decoded_frames[1].height() == height
|
|
507
|
+
assert decoded_frames[1].format == ImageFormat.I420
|
|
508
|
+
assert decoded_frames[1].timestamp == timedelta(microseconds=1000 + 33333)
|
|
509
|
+
|
|
510
|
+
# デコードされたフレームの内容を簡単に検証
|
|
511
|
+
# Y平面の最初と最後の値がグラデーションになっているか確認
|
|
512
|
+
decoded_y = decoded_frames[0].i420_buffer.y.reshape(height, width)
|
|
513
|
+
assert decoded_y[0, 0] < decoded_y[0, -1] # 左端より右端の方が明るい
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2025.1.0.dev8
|
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import time
|
|
3
|
-
from datetime import timedelta
|
|
4
|
-
|
|
5
|
-
import numpy as np
|
|
6
|
-
import pytest
|
|
7
|
-
|
|
8
|
-
from libdatachannel.codec import (
|
|
9
|
-
EncodedImage,
|
|
10
|
-
ImageFormat,
|
|
11
|
-
VideoCodecType,
|
|
12
|
-
VideoEncoder,
|
|
13
|
-
VideoFrame,
|
|
14
|
-
VideoFrameBufferBGR888,
|
|
15
|
-
VideoFrameBufferI420,
|
|
16
|
-
VideoFrameBufferNV12,
|
|
17
|
-
create_aom_video_encoder,
|
|
18
|
-
create_openh264_video_encoder,
|
|
19
|
-
create_videotoolbox_video_encoder,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_video_frame_buffer():
|
|
24
|
-
# I420
|
|
25
|
-
i420_buffer = VideoFrameBufferI420.create(640, 480)
|
|
26
|
-
assert i420_buffer.width() == 640
|
|
27
|
-
assert i420_buffer.height() == 480
|
|
28
|
-
assert i420_buffer.stride_y() == 640
|
|
29
|
-
assert i420_buffer.stride_u() == 320
|
|
30
|
-
assert i420_buffer.stride_v() == 320
|
|
31
|
-
assert isinstance(i420_buffer.y, np.ndarray)
|
|
32
|
-
assert isinstance(i420_buffer.u, np.ndarray)
|
|
33
|
-
assert isinstance(i420_buffer.v, np.ndarray)
|
|
34
|
-
i420_buffer.y[0] = 255
|
|
35
|
-
i420_buffer.y = np.zeros((100, 100), dtype=np.uint8)
|
|
36
|
-
assert i420_buffer.width() == 100
|
|
37
|
-
|
|
38
|
-
# NV12
|
|
39
|
-
nv12_buffer = VideoFrameBufferNV12.create(640, 480)
|
|
40
|
-
assert nv12_buffer.width() == 640
|
|
41
|
-
assert nv12_buffer.height() == 480
|
|
42
|
-
assert nv12_buffer.stride_y() == 640
|
|
43
|
-
assert nv12_buffer.stride_uv() == 640
|
|
44
|
-
assert isinstance(nv12_buffer.y, np.ndarray)
|
|
45
|
-
assert isinstance(nv12_buffer.uv, np.ndarray)
|
|
46
|
-
|
|
47
|
-
# BGR888
|
|
48
|
-
bgr888_buffer = VideoFrameBufferBGR888.create(640, 480)
|
|
49
|
-
assert bgr888_buffer.width() == 640
|
|
50
|
-
assert bgr888_buffer.height() == 480
|
|
51
|
-
assert bgr888_buffer.stride() == 640 * 3
|
|
52
|
-
assert isinstance(bgr888_buffer.bgr, np.ndarray)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_video_frame_properties():
|
|
56
|
-
frame = VideoFrame()
|
|
57
|
-
|
|
58
|
-
# format による width()/height() の分岐
|
|
59
|
-
frame.i420_buffer = VideoFrameBufferI420.create(640, 360)
|
|
60
|
-
frame.nv12_buffer = VideoFrameBufferNV12.create(1280, 720)
|
|
61
|
-
frame.bgr888_buffer = VideoFrameBufferBGR888.create(1920, 1080)
|
|
62
|
-
|
|
63
|
-
frame.format = ImageFormat.I420
|
|
64
|
-
assert frame.width() == 640
|
|
65
|
-
assert frame.height() == 360
|
|
66
|
-
|
|
67
|
-
frame.format = ImageFormat.NV12
|
|
68
|
-
assert frame.width() == 1280
|
|
69
|
-
assert frame.height() == 720
|
|
70
|
-
|
|
71
|
-
frame.format = ImageFormat.BGR888
|
|
72
|
-
assert frame.width() == 1920
|
|
73
|
-
assert frame.height() == 1080
|
|
74
|
-
|
|
75
|
-
# timestamp, rid
|
|
76
|
-
ts = timedelta(microseconds=1234567)
|
|
77
|
-
frame.timestamp = ts
|
|
78
|
-
assert frame.timestamp == ts
|
|
79
|
-
|
|
80
|
-
frame.rid = "abc"
|
|
81
|
-
assert frame.rid == "abc"
|
|
82
|
-
|
|
83
|
-
# base_width, base_height
|
|
84
|
-
frame.base_width = 999
|
|
85
|
-
frame.base_height = 888
|
|
86
|
-
assert frame.base_width == 999
|
|
87
|
-
assert frame.base_height == 888
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def test_encoded_image_properties():
|
|
91
|
-
img = EncodedImage()
|
|
92
|
-
|
|
93
|
-
img.data = np.array([], dtype=np.uint8)
|
|
94
|
-
assert img.data.size == 0
|
|
95
|
-
img.data = np.array([1, 2, 3], dtype=np.uint8)
|
|
96
|
-
assert img.data.size == 3
|
|
97
|
-
|
|
98
|
-
ts = timedelta(microseconds=555000)
|
|
99
|
-
img.timestamp = ts
|
|
100
|
-
assert img.timestamp == ts
|
|
101
|
-
|
|
102
|
-
img.rid = "r0"
|
|
103
|
-
assert img.rid == "r0"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_openh264():
|
|
107
|
-
openh264 = os.environ.get("OPENH264_PATH")
|
|
108
|
-
assert openh264 is not None
|
|
109
|
-
encoder = create_openh264_video_encoder(openh264)
|
|
110
|
-
settings = VideoEncoder.Settings()
|
|
111
|
-
settings.codec_type = VideoCodecType.H264
|
|
112
|
-
settings.width = 640
|
|
113
|
-
settings.height = 480
|
|
114
|
-
settings.bitrate = 1000000
|
|
115
|
-
success = encoder.init(settings)
|
|
116
|
-
assert success
|
|
117
|
-
frame = VideoFrame()
|
|
118
|
-
frame.i420_buffer = VideoFrameBufferI420.create(640, 360)
|
|
119
|
-
frame.format = ImageFormat.I420
|
|
120
|
-
frame.base_width = 640
|
|
121
|
-
frame.base_height = 480
|
|
122
|
-
frame.timestamp = timedelta(microseconds=1234567)
|
|
123
|
-
|
|
124
|
-
on_encode_called = False
|
|
125
|
-
|
|
126
|
-
def on_encode(encoded_image):
|
|
127
|
-
nonlocal on_encode_called
|
|
128
|
-
assert encoded_image.data.size > 0
|
|
129
|
-
assert encoded_image.timestamp == frame.timestamp
|
|
130
|
-
assert encoded_image.rid == frame.rid
|
|
131
|
-
on_encode_called = True
|
|
132
|
-
|
|
133
|
-
encoder.set_on_encode(on_encode)
|
|
134
|
-
encoder.encode(frame)
|
|
135
|
-
encoder.release()
|
|
136
|
-
assert on_encode_called
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@pytest.mark.skipif(
|
|
140
|
-
os.environ.get("ENABLE_VIDEOTOOLBOX") is None, reason="macOS の場合だけ実行する"
|
|
141
|
-
)
|
|
142
|
-
def test_videotoolbox():
|
|
143
|
-
encoder = create_videotoolbox_video_encoder()
|
|
144
|
-
settings = VideoEncoder.Settings()
|
|
145
|
-
settings.codec_type = VideoCodecType.H264
|
|
146
|
-
settings.width = 640
|
|
147
|
-
settings.height = 480
|
|
148
|
-
settings.bitrate = 1000000
|
|
149
|
-
success = encoder.init(settings)
|
|
150
|
-
assert success
|
|
151
|
-
frame = VideoFrame()
|
|
152
|
-
frame.nv12_buffer = VideoFrameBufferNV12.create(640, 360)
|
|
153
|
-
frame.format = ImageFormat.NV12
|
|
154
|
-
frame.base_width = 640
|
|
155
|
-
frame.base_height = 480
|
|
156
|
-
frame.timestamp = timedelta(microseconds=1234567)
|
|
157
|
-
|
|
158
|
-
on_encode_called = False
|
|
159
|
-
|
|
160
|
-
def on_encode(encoded_image):
|
|
161
|
-
nonlocal on_encode_called
|
|
162
|
-
assert encoded_image.data.size > 0
|
|
163
|
-
assert encoded_image.timestamp == frame.timestamp
|
|
164
|
-
assert encoded_image.rid == frame.rid
|
|
165
|
-
on_encode_called = True
|
|
166
|
-
|
|
167
|
-
encoder.set_on_encode(on_encode)
|
|
168
|
-
encoder.encode(frame)
|
|
169
|
-
time.sleep(1)
|
|
170
|
-
encoder.release()
|
|
171
|
-
assert on_encode_called
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def test_aom():
|
|
175
|
-
encoder = create_aom_video_encoder()
|
|
176
|
-
settings = VideoEncoder.Settings()
|
|
177
|
-
settings.codec_type = VideoCodecType.H264
|
|
178
|
-
settings.width = 640
|
|
179
|
-
settings.height = 480
|
|
180
|
-
settings.bitrate = 1000000
|
|
181
|
-
success = encoder.init(settings)
|
|
182
|
-
assert success
|
|
183
|
-
frame = VideoFrame()
|
|
184
|
-
frame.i420_buffer = VideoFrameBufferI420.create(640, 360)
|
|
185
|
-
frame.format = ImageFormat.I420
|
|
186
|
-
frame.base_width = 640
|
|
187
|
-
frame.base_height = 480
|
|
188
|
-
frame.timestamp = timedelta(microseconds=1234567)
|
|
189
|
-
|
|
190
|
-
on_encode_called = False
|
|
191
|
-
|
|
192
|
-
def on_encode(encoded_image):
|
|
193
|
-
nonlocal on_encode_called
|
|
194
|
-
assert encoded_image.data.size > 0
|
|
195
|
-
assert encoded_image.timestamp == frame.timestamp
|
|
196
|
-
assert encoded_image.rid == frame.rid
|
|
197
|
-
on_encode_called = True
|
|
198
|
-
|
|
199
|
-
encoder.set_on_encode(on_encode)
|
|
200
|
-
encoder.encode(frame)
|
|
201
|
-
encoder.release()
|
|
202
|
-
assert on_encode_called
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_configuration.py
RENAMED
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_description.py
RENAMED
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_h265nalunit.py
RENAMED
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_mediahandler.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_peerconnection.py
RENAMED
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_reliability.py
RENAMED
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_rtppacketizer.py
RENAMED
|
File without changes
|
|
File without changes
|
{libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_websocketserver.py
RENAMED
|
File without changes
|