summaryrefslogtreecommitdiffstats
path: root/multimedia/gst1-rtsp-server/tests/model.uc
blob: a613063b3e95d03fc72baac29e8e82259965887c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import { mkdtemp, mkdir, writefile, readfile, symlink, rename, glob, unlink } from 'fs';
import * as uci from 'uci';
import { inventory_read, pipeline_build, path_normalise, stream_resolve,
	encoding_build, bitrate_default, stream_caps_parse } from '../files/model.uc';
import { config_update, stream_pipeline, camera_pipeline } from '../files/config.uc';

let directory = mkdtemp('/tmp/gst-rtsp-test-XXXXXX');
let root = directory + '/sys';

function directory_add(path) {
	let current = '';
	for (let part in split(path, '/')) {
		if (!part)
			continue;
		current += '/' + part;
		mkdir(current, 0700);
	}
}

directory_add(root + '/class/video4linux');
directory_add(root + '/bus/media/devices');
directory_add(root + '/devices/platform/isp');
directory_add(root + '/devices/platform/isp/media0');
directory_add(root + '/devices/platform/codec');
directory_add(root + '/devices/platform/converter');
directory_add(root + '/devices/platform/i2c/sensor');
directory_add(root + '/firmware/devicetree/base/camera0');
symlink(root + '/firmware/devicetree/base/camera0',
	root + '/devices/platform/i2c/sensor/of_node');
symlink(root + '/devices/platform/isp/media0', root + '/bus/media/devices/media0');
let endpoints = ['mainpath', 'parameters', 'statistics', 'encoder', 'converter'];
let mapping = [0, 1, 2, 3, 4];
for (let index, name in endpoints) {
	let path = root + '/class/video4linux/video' + index;
	directory_add(path);
	let parent = index == 4 ? 'converter' : (index == 3 ? 'codec' : 'isp');
	symlink(root + '/devices/platform/' + parent, path + '/device');
	writefile(path + '/name', name);
}
directory_add(root + '/class/video4linux/v4l-subdev0');
symlink(root + '/devices/platform/i2c/sensor', root + '/class/video4linux/v4l-subdev0/device');

function run(args) {
	if (args[0] == 'media-ctl')
		return `- entity 1: sensor (1 pad, 1 link)
  type V4L2 subdev subtype Sensor flags 0
  device node name /dev/v4l-subdev0
- entity 2: isp (2 pads, 4 links)
  type V4L2 subdev subtype Unknown flags 0
  pad0: SINK
    <- "sensor":0 [ENABLED,IMMUTABLE]
    <- "parameters":0 [ENABLED]
- entity 3: mainpath (1 pad, 1 link)
  type Node subtype V4L flags 0
  device node name /dev/video${mapping[0]}
  pad0: SINK
    <- "isp":1 [ENABLED]
- entity 4: parameters (1 pad, 1 link)
  type Node subtype V4L flags 0
  device node name /dev/video${mapping[1]}
- entity 5: statistics (1 pad, 1 link)
  type Node subtype V4L flags 0
  device node name /dev/video${mapping[2]}
  pad0: SINK
    <- "isp":1 [ENABLED]
`;
	if (args[0] == 'gst-device-monitor-1.0')
		return `Device found:
  name : /base/camera0
  class : Source/Video
  caps : video/x-raw
  properties:
    api.libcamera.PixelArraySize = < (int)4056, (int)3040 > (GstValueArray)
  gst-launch-1.0 libcamerasrc camera-name='/base/camera0' ! ...
`;
	if (args[0] == 'gst-inspect-1.0') {
		if (args[1] == '--plugin')
			return '  v4l2h265enc: V4L2 H265 Encoder\n' +
				'  v4l2h264enc: V4L2 H264 Encoder\n  v4l2jpegenc: V4L2 JPEG Encoder\n' +
				'  v4l2convert: V4L2 Converter\n';
		let device = args[1] == 'v4l2convert' ? mapping[4] : mapping[3];
		return `  device              : Device location
                        flags: readable, writable
                        String. Default: "/dev/video${device}"
`;
	}
	if (args[2] == '/dev/video11') {
		if (args[3] == '-D')
			return 'Device Caps : 0x04000001';
		return args[3] == '--list-formats' ? "[0]: 'NV12' (Y/CbCr 4:2:0)" : '';
	}
	if (index(['/dev/video20', '/dev/video21', '/dev/video22'], args[2]) >= 0)
		return args[3] == '-D' ? 'Device Caps : 0x05200002' : '';
	let device = int(substr(args[2], length('/dev/video')));
	let role = index(mapping, device);
	if (args[3] == '-D')
		return 'Device Caps : ' + ['0x24201000', '0x04002000', '0x04800000',
			'0x04004000', '0x04004000'][role];
	if (args[3] == '--list-formats-out')
		return role >= 3 ? "[0]: 'NV12' (Y/CbCr 4:2:0)" : '';
	if (args[3] == '--list-ctrls-menus')
		return role == 3 ?
			'compression_quality 0x009d0903 (int) : min=1 max=100 step=1 default=80 value=80\n' +
			'video_bitrate_mode 0x009909ce (menu) : min=0 max=1 default=1 value=1\n' +
			'  1: Constant Bitrate' : '';
	return role == 3 ? "[0]: 'HEVC' (HEVC)\n[1]: 'H264' (H264)\n[2]: 'MJPG' (JPEG)" :
		"[0]: 'NV12' (Y/CbCr 4:2:0)";
}

let inventory = inventory_read(root, run);
assert(length(inventory.devices) == 7);
assert(length(inventory.cameras) == 1);
assert(length(inventory.cameras[0].devices) == 5);
assert(length(filter(values(inventory.devices), n => n.role == 'metadata')) == 1);
assert(length(filter(values(inventory.devices), n => n.role == 'encoder')) == 1);
assert(inventory.encoders[0].codec == 'h265');
assert(inventory.processors[0].factory == 'v4l2convert');
assert(path_normalise('pci0000/usb3/3-2/3-2.4:1.0') == 'pci0000/usb/2/2.4:1.0');

directory_add(directory + '/config');
directory_add(directory + '/delta');
writefile(directory + '/config/gst-rtsp', "config service 'main'\n");
let cursor = uci.cursor(directory + '/config', directory + '/delta', '');
assert(config_update(cursor, inventory));
let config = cursor.get_all('gst-rtsp');
assert(length(filter(values(config), s => s['.type'] == 'stream')) == 1);
assert(length(filter(values(config), s => s['.type'] == 'video-device' && s.handled == '1')) == 7);
assert(cursor.get('gst-rtsp', 'camera0', 'configured') == '1');
assert(cursor.get('gst-rtsp', 'camera0', 'sensor') != null);
assert(cursor.get('gst-rtsp', 'input0', 'enabled') == '0');
assert(cursor.get('gst-rtsp', 'stream0', 'bitrate_mode') == 'cbr');
assert(cursor.get('gst-rtsp', 'stream0', 'bitrate') == '0');
assert(index(readfile(directory + '/config/gst-rtsp'), '/dev/video') < 0);
cursor.set('gst-rtsp', 'stream0', 'width', '1920');
cursor.set('gst-rtsp', 'stream0', 'resolution', 'custom');
cursor.set('gst-rtsp', 'stream0', 'bitrate_mode', 'auto');
cursor.set('gst-rtsp', 'stream0', 'bitrate', '12345678');
cursor.commit('gst-rtsp');
let saved = readfile(directory + '/config/gst-rtsp');
assert(!config_update(cursor, inventory));
assert(readfile(directory + '/config/gst-rtsp') == saved);

for (let i in mapping)
	rename(root + '/class/video4linux/video' + i, root + '/class/video4linux/tmp' + i);
mapping = [7, 4, 9, 2, 5];
for (let i, number in mapping)
	rename(root + '/class/video4linux/tmp' + i, root + '/class/video4linux/video' + number);
let renumbered = inventory_read(root, run);
assert(join('\n', sort(keys(renumbered.devices))) == join('\n', sort(keys(inventory.devices))));
assert(!config_update(cursor, renumbered));
assert(readfile(directory + '/config/gst-rtsp') == saved);
assert(renumbered.devices[renumbered.encoders[0].key].node == '/dev/video2');

let automatic = stream_resolve({ codec: 'auto', resolution: 'native' }, {},
	{ path: '/base/camera0' }, renumbered);
assert(automatic.codec == 'h265' && automatic.width == 4056 && automatic.height == 3040);
assert(stream_resolve({ codec: 'auto' }, {}, {}, {
	encoders: filter(renumbered.encoders, e => e.codec != 'h265')
}).codec == 'h264');
let encoder_controls = renumbered.devices[renumbered.encoders[0].key].controls;
encoder_controls.frame_level_rate_control_enable = { type: 'bool', default: 1 };
encoder_controls.hevc_i_frame_qp_value = { min: 1, max: 51, default: 26 };
encoder_controls.hevc_p_frame_qp_value = { min: 1, max: 51, default: 28 };
let quality_pipeline = encoding_build({ ...automatic, bitrate_mode: 'quality', quality: 'high' },
	renumbered);
assert(index(quality_pipeline, 'frame_level_rate_control_enable=(int)0') >= 0);
assert(index(quality_pipeline, 'hevc_i_frame_qp_value=(int)20') >= 0);
assert(index(quality_pipeline, 'hevc_p_frame_qp_value=(int)22') >= 0);
assert(index(quality_pipeline, 'video_bitrate=') < 0);
let derived_pipeline = encoding_build({ ...automatic, bitrate_mode: 'cbr', bitrate: '0' },
	renumbered);
assert(index(derived_pipeline, 'video_bitrate=(int)22194432') >= 0);
assert(index(derived_pipeline, 'video_bitrate_mode=(int)1') >= 0);
assert(bitrate_default({ width: '1280', height: '720', framerate: '25/1' }, 'h264') == 2764800);
assert(bitrate_default({ width: '1280', height: '720' }, 'mjpeg') == 0);
let caps = stream_caps_parse('/GstPipeline:pipeline0/GstUnixFdSink:stream_stream0.GstPad:sink: caps = video/x-h265, width=(int)4056, height=(int)3040, framerate=(fraction)24000/1001, profile=(string)main');
assert(caps.name == 'stream0' && caps.width == '4056' && caps.framerate == '24000/1001');

cursor.set('gst-rtsp', 'input0', 'enabled', '1');
cursor.set('gst-rtsp', 'stream0', 'enabled', '1');
let launch = stream_pipeline(cursor, 'stream0', renumbered);
assert(index(launch, 'unixfdsrc socket-type=abstract socket-path="gst-rtsp.stream0"') >= 0);
assert(index(launch, 'h265parse ! rtph265pay name=pay0') >= 0);
let acquisition = camera_pipeline(cursor, 'camera0', renumbered);
assert(index(acquisition, 'camera-name="/base/camera0"') >= 0);
assert(index(acquisition, 'width=1920') >= 0);
assert(index(acquisition, 'video_bitrate=(int)12345678') >= 0);
assert(index(acquisition, 'v4l2h265enc output-io-mode=dmabuf-import') >= 0);
assert(index(acquisition, 'unixfdsink socket-type=abstract socket-path="gst-rtsp.stream0"') >= 0);
assert(index(acquisition, 'udpsink') < 0);
assert(index(acquisition, 'encodebin') < 0);
assert(index(acquisition, 'videoconvert') < 0);
cursor.set('gst-rtsp', 'stream0', 'bitrate_mode', 'cbr');
assert(index(camera_pipeline(cursor, 'camera0', renumbered),
	'video_bitrate_mode=(int)1') >= 0);
cursor.set('gst-rtsp', 'stream0', 'bitrate_mode', 'vbr');
let unsupported_mode;
try {
	camera_pipeline(cursor, 'camera0', renumbered);
} catch (e) {
	unsupported_mode = index('' + e, 'does not support the requested bitrate mode') >= 0;
}
assert(unsupported_mode, 'An unsupported bitrate mode must be rejected');
cursor.delete('gst-rtsp', 'stream0', 'bitrate_mode');
cursor.set('gst-rtsp', 'stream1', 'stream');
for (let key, value in { input: 'input0', enabled: '1', port: '8555', codec: 'mjpeg',
	width: '640', height: '480', jpeg_quality: '70' })
	cursor.set('gst-rtsp', 'stream1', key, value);
let neighbouring = stream_pipeline(cursor, 'stream1', renumbered);
let parallel = camera_pipeline(cursor, 'camera0', renumbered);
assert(index(parallel, 'camera.src_0 ! video/x-raw,width=640,height=480') >= 0);
assert(index(parallel, 'compression_quality=(int)70') >= 0);
cursor.set('gst-rtsp', 'stream2', 'stream');
for (let key, value in { input: 'input0', enabled: '1', port: '8556', codec: 'h264',
	width: '640', height: '480' })
	cursor.set('gst-rtsp', 'stream2', key, value);
let shared = camera_pipeline(cursor, 'camera0', renumbered);
assert(length(split(shared, 'tee name=capture_')) == 3);
assert(length(split(shared, 'capture_1. ! video/x-raw,width=640,height=480')) == 3);
assert(index(shared, 'camera.src_1') < 0);
cursor.delete('gst-rtsp', 'stream2');
for (let key, value in { width: '3840', height: '2160', codec: 'h264',
	profile: 'high', bitrate: '16000000', keyframe_interval: '30' })
	cursor.set('gst-rtsp', 'stream0', key, value);
assert(camera_pipeline(cursor, 'camera0', renumbered) != parallel);
assert(stream_pipeline(cursor, 'stream1', renumbered) == neighbouring);
assert(stream_pipeline(cursor, 'stream0', renumbered) != launch);
assert(index(neighbouring, 'rtpjpegpay name=pay0') >= 0);
let processor = filter(values(config), s => s.role == 'processor')[0]['.name'];
cursor.set('gst-rtsp', 'stream0', 'processor', [processor]);
assert(index(camera_pipeline(cursor, 'camera0', renumbered),
	'v4l2convert disable-passthrough=true output-io-mode=dmabuf-import') >= 0);
cursor.set('gst-rtsp', 'input0', 'processor', [processor]);
assert(index(camera_pipeline(cursor, 'camera0', renumbered),
	'camera.src ! v4l2convert disable-passthrough=true') >= 0);
cursor.delete('gst-rtsp', 'input0', 'processor');
cursor.delete('gst-rtsp', 'stream1');
cursor.delete('gst-rtsp', 'stream0');
cursor.commit('gst-rtsp');
assert(!config_update(cursor, renumbered));
assert(cursor.get('gst-rtsp', 'stream0') == null);

directory_add(root + '/devices/platform/xhci/usb3/3-2/3-2:1.0');
directory_add(root + '/class/video4linux/video11');
symlink(root + '/devices/platform/xhci/usb3/3-2/3-2:1.0',
	root + '/class/video4linux/video11/device');
writefile(root + '/class/video4linux/video11/name', 'USB camera');
let usb = inventory_read(root, run);
assert(length(usb.cameras) == 2);
assert(config_update(cursor, usb));
assert(cursor.get('gst-rtsp', 'camera1', 'source') == 'v4l2src');
cursor.set('gst-rtsp', 'input1', 'enabled', '1');
cursor.set('gst-rtsp', 'stream0', 'enabled', '1');
cursor.set('gst-rtsp', 'stream0', 'resolution', 'custom');
cursor.set('gst-rtsp', 'stream0', 'bitrate_mode', 'auto');
cursor.set('gst-rtsp', 'usb_extra', 'stream');
for (let key, value in { input: 'input1', enabled: '1', port: '8556', codec: 'h264' })
	cursor.set('gst-rtsp', 'usb_extra', key, value);
let usb_launch = camera_pipeline(cursor, 'camera1', usb);
assert(length(split(usb_launch, 'v4l2src')) == 2);
assert(index(usb_launch, 'device="/dev/video11" io-mode=dmabuf') >= 0);
assert(index(usb_launch, 'tee name=input1') >= 0);
assert(index(usb_launch, 'v4l2h265enc output-io-mode=dmabuf-import') >= 0);
assert(index(usb_launch, 'v4l2h264enc output-io-mode=dmabuf-import') >= 0);

let failed;
try {
	pipeline_build({ codec: 'h265' }, null, {});
} catch (e) {
	failed = true;
}
assert(failed, 'A missing hardware encoder must not select software');

function virtual_add(number) {
	let path = `${root}/devices/virtual/video4linux/video${number}`;
	directory_add(path);
	writefile(path + '/name', 'isp-hevc');
	symlink(path, `${root}/class/video4linux/video${number}`);
}

virtual_add(20);
let virtual = inventory_read(root, run);
let virtual_key = 'virtual/output/isp-hevc';
assert(virtual.devices[virtual_key].node == '/dev/video20');
assert(length(virtual.cameras) == length(usb.cameras));
assert(config_update(cursor, virtual));
let virtual_config = readfile(directory + '/config/gst-rtsp');
unlink(root + '/class/video4linux/video20');
virtual_add(21);
let virtual_renumbered = inventory_read(root, run);
assert(virtual_renumbered.devices[virtual_key].node == '/dev/video21');
assert(!config_update(cursor, virtual_renumbered));
assert(readfile(directory + '/config/gst-rtsp') == virtual_config);
virtual_add(22);
failed = false;
try {
	inventory_read(root, run);
} catch (e) {
	failed = index('' + e, 'Ambiguous video identity') >= 0;
}
assert(failed, 'Duplicate virtual card identities must be rejected');
print('Topology, UCI preservation, renumbering, deletion and hardware pipeline checks passed\n');
print('Fixture: ', directory, '\n');