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.
Files changed (39) hide show
  1. {libdatachannel_py-2025.1.0.dev8/src/libdatachannel_py.egg-info → libdatachannel_py-2025.1.0.dev9}/PKG-INFO +2 -1
  2. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/README.md +1 -0
  3. libdatachannel_py-2025.1.0.dev9/VERSION +1 -0
  4. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/pyproject.toml +7 -2
  5. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/libdatachannel_ext.cpython-313-darwin.so +0 -0
  6. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9/src/libdatachannel_py.egg-info}/PKG-INFO +2 -1
  7. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel_py.egg-info/SOURCES.txt +1 -0
  8. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_audio_codec.py +56 -2
  9. libdatachannel_py-2025.1.0.dev9/tests/test_prop_audio_codec.py +97 -0
  10. libdatachannel_py-2025.1.0.dev9/tests/test_video_codec.py +513 -0
  11. libdatachannel_py-2025.1.0.dev8/VERSION +0 -1
  12. libdatachannel_py-2025.1.0.dev8/tests/test_video_codec.py +0 -202
  13. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/LICENSE +0 -0
  14. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/MANIFEST.in +0 -0
  15. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/buildbase.py +0 -0
  16. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/pypath.py +0 -0
  17. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/run.py +0 -0
  18. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/setup.cfg +0 -0
  19. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/setup.py +0 -0
  20. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/__init__.py +0 -0
  21. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/libdatachannel_ext.pyi +0 -0
  22. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel/py.typed +0 -0
  23. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel_py.egg-info/dependency_links.txt +0 -0
  24. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/src/libdatachannel_py.egg-info/top_level.txt +0 -0
  25. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_candidate.py +0 -0
  26. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_configuration.py +0 -0
  27. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_description.py +0 -0
  28. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_frameinfo.py +0 -0
  29. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_h265nalunit.py +0 -0
  30. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_libyuv.py +0 -0
  31. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_mediahandler.py +0 -0
  32. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_message.py +0 -0
  33. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_nalunit.py +0 -0
  34. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_packetizationconfig.py +0 -0
  35. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_peerconnection.py +0 -0
  36. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_reliability.py +0 -0
  37. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_rtppacketizer.py +0 -0
  38. {libdatachannel_py-2025.1.0.dev8 → libdatachannel_py-2025.1.0.dev9}/tests/test_websocket.py +0 -0
  39. {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.dev8
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.8", "wheel>=0.45.1"]
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.8",
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: libdatachannel-py
3
- Version: 2025.1.0.dev8
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
 
@@ -27,6 +27,7 @@ tests/test_message.py
27
27
  tests/test_nalunit.py
28
28
  tests/test_packetizationconfig.py
29
29
  tests/test_peerconnection.py
30
+ tests/test_prop_audio_codec.py
30
31
  tests/test_reliability.py
31
32
  tests/test_rtppacketizer.py
32
33
  tests/test_video_codec.py
@@ -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 samples
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 → 20ms でエンコード
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