@linanw/homebridge-mijia-remote-camera 0.2.0 → 0.2.1
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.
- package/README.md +2 -2
- package/homebridge-ui/public/index.html +4 -0
- package/lib/matter-device.mjs +69 -19
- package/lib/matter-diagnostics.mjs +3 -0
- package/lib/matter-media.mjs +28 -8
- package/lib/platform.mjs +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ mijia-camera-setup --from /homebridge/homebridge-mijia-remote-camera-0.1.0.tgz -
|
|
|
64
64
|
|
|
65
65
|
夜视目前仅支持已核对 MIOT 定义的 `chuangmi.camera.061a01`/`chuangmi.camera.061a03`。插件设置页先读取设备实际状态,再选择“自动/开启/关闭”并应用;写入后会回读确认。Apple“家庭”的“夜视”开关表示允许夜视:自动模式显示为开,重复开不会把自动改为强制开启;关掉后重新打开会设置为始终开启。需要恢复自动时,请在插件设置页选择。该开关表示夜视策略,不表示红外灯此刻是否点亮。启动和轮询只读状态,不更改策略。
|
|
66
66
|
|
|
67
|
-
## Matter 摄像头(0.2.
|
|
67
|
+
## Matter 摄像头(0.2.1,实验性)
|
|
68
68
|
|
|
69
69
|
可在保留现有 HomeKit 配对的同时启用独立的 Matter Camera(设备类型 `0x0142`,端点 `1`)。它实现 Camera AV Stream Management 与 WebRTC Transport Provider,包含 H.264 视频、Opus 现场声音、双向对讲、JPEG 快照、音量/静音和支持型号的夜视模式。当前使用 matter.js 0.18.0 的 Matter 1.6.1 模型,已与 matter-server 1.5.3 控制器进行协议对接。MatterDesk Qt 客户端是当前目标客户端;客户端需要实现摄像头播放。Apple“家庭”仍使用本插件原有的 HomeKit 配件。
|
|
70
70
|
|
|
@@ -79,7 +79,7 @@ mijia-camera-setup --from /homebridge/homebridge-mijia-remote-camera-0.1.0.tgz -
|
|
|
79
79
|
}
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
0.2.1 修复了仅视频/仅音频会话的资源清理,并支持浏览器早到的 ICE 候选、重复启动,以及 Chromium 多 H264 profile 的正常协商。升级默认不开启 Matter。启用后,从受保护的插件设置页点击显示 Matter 配对信息,将手动配对码或 `MT:` 内容输入控制器。它与 HomeKit Setup Code 不同。已配对后增加其他控制器时可在设置页重新打开配对窗口,操作完成后可以关闭窗口。配对码和 Matter 私钥不会写入脱敏报告。Matter fabric/设备身份保存在插件私有存储的 `matter/` 内,升级和正常重启保留身份;删除该目录会使已有配对失效。
|
|
83
83
|
|
|
84
84
|
这是未认证的开发设备,使用 SDK 的测试 VID/开发证书。matter-server 默认会拒绝这种证书;只在明确信任此开发设备时,使用它的 `--enable-test-net-dcl`(或 `ENABLE_TEST_NET_DCL=true`)选项。插件不冒充通过认证的产品,客户端普通启动应保留严格验证。
|
|
85
85
|
|
|
@@ -100,6 +100,10 @@
|
|
|
100
100
|
if(record(value.media)){
|
|
101
101
|
const media={};for(const key of ['sessions','failures','videoPackets','audioPackets','talkPackets','talkBytes'])if(count(value.media[key]))media[key]=value.media[key];
|
|
102
102
|
for(const key of ['active','microphoneMuted','speakerMuted'])if(typeof value.media[key]==='boolean')media[key]=value.media[key];
|
|
103
|
+
const source=value.media;
|
|
104
|
+
for (const [key, allowed] of Object.entries({"lastFailure": ["negotiation_failed", "media_not_offered", "webrtc_timeout", "webrtc_failed", "webrtc_disconnected", "media_unavailable", "udp_error", "ffmpeg_unavailable", "ffmpeg_exit", "video_timeout"], "lastFailureStage": ["remote_description", "replace_video_track", "replace_audio_track", "create_answer", "create_offer", "local_description", "remote_answer"], "lastFailureType": ["Error", "TypeError", "RangeError", "OperationError", "InvalidStateError", "InvalidModificationError", "InvalidAccessError", "NotSupportedError", "SyntaxError", "AbortError"]})) {
|
|
105
|
+
if (allowed.includes(source[key])) media[key] = source[key];
|
|
106
|
+
}
|
|
103
107
|
if(Object.keys(media).length)result.media=media;
|
|
104
108
|
}
|
|
105
109
|
return result;
|
package/lib/matter-device.mjs
CHANGED
|
@@ -75,7 +75,7 @@ export class MatterCameraResources {
|
|
|
75
75
|
const id = this.nextSession;
|
|
76
76
|
this.nextSession = this.nextSession % 65534 + 1;
|
|
77
77
|
const item = { id, owner: { ...owner }, peerEndpointId, video, audio, state: 'connecting', pendingCandidates: [], ready: false,
|
|
78
|
-
mediaId: `${owner.fabricIndex}:${owner.nodeId}:${id}` };
|
|
78
|
+
pendingRemoteCandidates: [], mediaCreated: false, mediaId: `${owner.fabricIndex}:${owner.nodeId}:${id}` };
|
|
79
79
|
this.sessions.set(id, item);
|
|
80
80
|
for (const entry of [video, audio]) if (entry) entry.stream.referenceCount++;
|
|
81
81
|
return item;
|
|
@@ -85,6 +85,8 @@ export class MatterCameraResources {
|
|
|
85
85
|
if (this.sessions.get(item.id) !== item || item.ending) return false;
|
|
86
86
|
item.ending = true;
|
|
87
87
|
clearTimeout(item.timer);
|
|
88
|
+
item.pendingCandidates.length = 0;
|
|
89
|
+
item.pendingRemoteCandidates.length = 0;
|
|
88
90
|
this.sessions.delete(item.id);
|
|
89
91
|
for (const entry of [item.video, item.audio]) if (entry) entry.stream.referenceCount = Math.max(0, entry.stream.referenceCount - 1);
|
|
90
92
|
return true;
|
|
@@ -136,6 +138,42 @@ export function validateMatterTransportRequest(request) {
|
|
|
136
138
|
reject(Status.InvalidCommand, 'Requestor endpoint is required');
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
export function describeMatterSession(item) {
|
|
142
|
+
return { id: item.id, peerNodeId: item.owner.nodeId, peerEndpointId: item.peerEndpointId,
|
|
143
|
+
fabricIndex: item.owner.fabricIndex, streamUsage: LIVE, metadataEnabled: false,
|
|
144
|
+
videoStreamId: item.video?.stream.videoStreamId ?? null, audioStreamId: item.audio?.stream.audioStreamId ?? null,
|
|
145
|
+
...(item.video ? { videoStreams: [item.video.stream.videoStreamId] } : {}),
|
|
146
|
+
...(item.audio ? { audioStreams: [item.audio.stream.audioStreamId] } : {}) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Keep external encoder accounting atomic with Matter's own cluster-state transaction. */
|
|
150
|
+
export function enrollMatterSession({ resources, item, transaction, publish, start, afterRollback,
|
|
151
|
+
schedule = setImmediate }) {
|
|
152
|
+
let launchScheduled = false;
|
|
153
|
+
const rollback = () => { resources.detachSession(item); };
|
|
154
|
+
try {
|
|
155
|
+
transaction.addParticipants({
|
|
156
|
+
toString: () => `camera-session-create#${item.id}`,
|
|
157
|
+
rollback,
|
|
158
|
+
conclusion(outcome) {
|
|
159
|
+
if (outcome === 'committed' && !launchScheduled) {
|
|
160
|
+
launchScheduled = true;
|
|
161
|
+
schedule(() => { if (resources.sessions.get(item.id) === item && !item.ending) start(); });
|
|
162
|
+
} else if (outcome !== 'committed') {
|
|
163
|
+
rollback();
|
|
164
|
+
if (afterRollback) schedule(afterRollback);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
publish();
|
|
169
|
+
} catch (error) {
|
|
170
|
+
rollback();
|
|
171
|
+
// The enclosing transaction also rolls back. Restore its view immediately when synchronous validation failed.
|
|
172
|
+
try { publish(); } catch {}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
/**
|
|
140
178
|
* A real Matter Camera (0x0142), using the Matter 1.6.1 model in matter.js 0.18.
|
|
141
179
|
* Matter carries authenticated signaling; media is encrypted WebRTC supplied by matter-media.
|
|
@@ -206,11 +244,7 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
206
244
|
}
|
|
207
245
|
function streamList(kind) { return [...allocated[kind].values()].map(item => clone(item.stream)); }
|
|
208
246
|
function describeSession(item) {
|
|
209
|
-
return
|
|
210
|
-
fabricIndex: item.owner.fabricIndex, streamUsage: LIVE, metadataEnabled: false,
|
|
211
|
-
videoStreamId: item.video?.stream.videoStreamId ?? null, audioStreamId: item.audio?.stream.audioStreamId ?? null,
|
|
212
|
-
videoStreams: item.video ? [item.video.stream.videoStreamId] : [],
|
|
213
|
-
audioStreams: item.audio ? [item.audio.stream.audioStreamId] : [] };
|
|
247
|
+
return describeMatterSession(item);
|
|
214
248
|
}
|
|
215
249
|
async function syncState() {
|
|
216
250
|
if (!endpoint || closed) return;
|
|
@@ -251,6 +285,7 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
251
285
|
if (notify) await invokeRequestor(item, 'end', { webRtcSessionId: item.id, reason }).catch(() => {});
|
|
252
286
|
}
|
|
253
287
|
function fail(item, code) {
|
|
288
|
+
if (sessions.get(item.id) !== item) return;
|
|
254
289
|
lastError = code;
|
|
255
290
|
void end(item, code === 'matter_session_timeout' ? 1 : 0, true);
|
|
256
291
|
}
|
|
@@ -352,11 +387,13 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
352
387
|
if (isOffer && (typeof request.sdp !== 'string' || request.sdp.length > 65535)) reject(Status.ConstraintError, 'Invalid SDP');
|
|
353
388
|
const item = resources.openSession({ owner, peerEndpointId: request.originatingEndpointId, video, audio }), { id } = item;
|
|
354
389
|
lastError = undefined;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
390
|
+
const startMedia = () => {
|
|
391
|
+
item.timer = setTimeout(() => fail(item, 'matter_session_timeout'), 30000);
|
|
392
|
+
item.timer.unref?.();
|
|
393
|
+
void (async () => {
|
|
358
394
|
if (closed || !sessions.has(id)) return;
|
|
359
395
|
await syncState();
|
|
396
|
+
if (closed || sessions.get(id) !== item || item.ending) return;
|
|
360
397
|
const v = video?.stream, a = audio?.stream;
|
|
361
398
|
const result = await media.createSession({ id: item.mediaId, ...(isOffer ? { offer: request.sdp } : {}),
|
|
362
399
|
video: v ? { id: v.videoStreamId, codec: 'h264', width: Math.min(width, v.maxResolution.width),
|
|
@@ -365,13 +402,15 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
365
402
|
audio: a ? { id: a.audioStreamId, codec: 'opus', sampleRate: 48000, channels: 1, bitRate: a.bitRate } : undefined,
|
|
366
403
|
iceServers: request.iceServers, iceTransportPolicy: request.iceTransportPolicy || 'all',
|
|
367
404
|
onCandidate(candidate) {
|
|
368
|
-
if (!candidate?.candidate || closed ||
|
|
405
|
+
if (!candidate?.candidate || closed || sessions.get(id) !== item) return;
|
|
369
406
|
const normalized = { candidate: candidate.candidate, sdpMid: candidate.sdpMid ?? null,
|
|
370
407
|
sdpmLineIndex: candidate.sdpMLineIndex ?? candidate.sdpmLineIndex ?? null };
|
|
371
|
-
if (!item.ready)
|
|
372
|
-
|
|
408
|
+
if (!item.ready) {
|
|
409
|
+
if (item.pendingCandidates.length < 128) item.pendingCandidates.push(normalized);
|
|
410
|
+
} else void invokeRequestor(item, 'iceCandidates', { webRtcSessionId: id, iceCandidates: [normalized] }).catch(() => fail(item, 'matter_signaling_failed'));
|
|
373
411
|
},
|
|
374
412
|
onState(state) {
|
|
413
|
+
if (closed || sessions.get(id) !== item || item.ending) return;
|
|
375
414
|
item.state = state;
|
|
376
415
|
if (state === 'connected') {
|
|
377
416
|
clearTimeout(item.timer);
|
|
@@ -379,16 +418,22 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
379
418
|
item.timer.unref?.();
|
|
380
419
|
} else if (state === 'failed' || state === 'closed') void end(item, state === 'failed' ? 0 : 2, true);
|
|
381
420
|
} });
|
|
382
|
-
if (
|
|
421
|
+
if (sessions.get(id) !== item || closed) { await media.closeSession(item.mediaId); return; }
|
|
422
|
+
item.mediaCreated = true;
|
|
423
|
+
while (item.pendingRemoteCandidates.length) await media.addCandidates(item.mediaId, item.pendingRemoteCandidates.splice(0, 32));
|
|
383
424
|
const sdp = isOffer ? result.answer : result.offer;
|
|
384
425
|
if (typeof sdp !== 'string' || !sdp.length) throw new Error('matter_signaling_failed');
|
|
385
426
|
await invokeRequestor(item, isOffer ? 'answer' : 'offer', { webRtcSessionId: id, sdp });
|
|
386
427
|
item.ready = true;
|
|
387
428
|
if (item.pendingCandidates.length) await invokeRequestor(item, 'iceCandidates', {
|
|
388
429
|
webRtcSessionId: id, iceCandidates: item.pendingCandidates.splice(0) });
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
item.
|
|
430
|
+
})().catch(() => fail(item, 'matter_signaling_failed'));
|
|
431
|
+
};
|
|
432
|
+
enrollMatterSession({ resources, item, transaction: this.context.transaction,
|
|
433
|
+
publish: () => { this.state.currentSessions = [...sessions.values()].map(describeSession); },
|
|
434
|
+
start: startMedia, afterRollback: () => {
|
|
435
|
+
void Promise.resolve(media.closeSession(item.mediaId)).catch(() => {}).then(() => syncState()).catch(() => {});
|
|
436
|
+
} });
|
|
392
437
|
const response = { webRtcSessionId: id };
|
|
393
438
|
if (!isOffer) response.deferredOffer = false;
|
|
394
439
|
if ('videoStreamId' in request) response.videoStreamId = video?.stream.videoStreamId ?? null;
|
|
@@ -405,8 +450,13 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
405
450
|
async provideIceCandidates({ webRtcSessionId, iceCandidates }) {
|
|
406
451
|
const item = sessionFor(this, webRtcSessionId);
|
|
407
452
|
if (!iceCandidates.length || iceCandidates.length > 32) reject(Status.ConstraintError, 'Invalid ICE candidates');
|
|
408
|
-
|
|
409
|
-
sdpMid: candidate.sdpMid ?? undefined, sdpMLineIndex: candidate.sdpmLineIndex ?? undefined }))
|
|
453
|
+
const normalized = iceCandidates.map(candidate => ({ candidate: candidate.candidate,
|
|
454
|
+
sdpMid: candidate.sdpMid ?? undefined, sdpMLineIndex: candidate.sdpmLineIndex ?? undefined }));
|
|
455
|
+
if (!item.mediaCreated) {
|
|
456
|
+
if (item.pendingRemoteCandidates.length + normalized.length > 128) reject(Status.ResourceExhausted, 'Too many pending ICE candidates');
|
|
457
|
+
item.pendingRemoteCandidates.push(...normalized); return;
|
|
458
|
+
}
|
|
459
|
+
try { await media.addCandidates(item.mediaId, normalized); }
|
|
410
460
|
catch { reject(Status.Failure, 'Invalid ICE candidate'); }
|
|
411
461
|
}
|
|
412
462
|
async endSession({ webRtcSessionId, reason }) {
|
|
@@ -425,7 +475,7 @@ export async function createMatterDevice({ storagePath, name, serialNumber, netw
|
|
|
425
475
|
...(config.listeningAddressIpv6 ? { listeningAddressIpv6: config.listeningAddressIpv6 } : {}) },
|
|
426
476
|
basicInformation: { vendorName: 'linanw', vendorId: VendorId(0xfff1), productName: 'Mijia Remote Camera',
|
|
427
477
|
productId: 0x8001, nodeLabel: text(name, 'Mijia Camera'), serialNumber: text(serialNumber, identifier),
|
|
428
|
-
hardwareVersion: 1, softwareVersion:
|
|
478
|
+
hardwareVersion: 1, softwareVersion: 201, softwareVersionString: '0.2.1' },
|
|
429
479
|
});
|
|
430
480
|
const audioCapabilities = { maxNumberOfChannels: 1, supportedCodecs: [0], supportedSampleRates: [48000], supportedBitDepths: [16] };
|
|
431
481
|
endpoint = new Endpoint(CameraDevice.with(MijiaAvServer, MijiaWebRtcServer, WebRtcTransportRequestorClient), {
|
|
@@ -23,6 +23,9 @@ export function safeMatterStatus(value = {}) {
|
|
|
23
23
|
if (count(source[key])) media[key] = source[key];
|
|
24
24
|
}
|
|
25
25
|
for (const key of ['active', 'microphoneMuted', 'speakerMuted']) if (typeof source[key] === 'boolean') media[key] = source[key];
|
|
26
|
+
for (const [key, allowed] of Object.entries({"lastFailure": ["negotiation_failed", "media_not_offered", "webrtc_timeout", "webrtc_failed", "webrtc_disconnected", "media_unavailable", "udp_error", "ffmpeg_unavailable", "ffmpeg_exit", "video_timeout"], "lastFailureStage": ["remote_description", "replace_video_track", "replace_audio_track", "create_answer", "create_offer", "local_description", "remote_answer"], "lastFailureType": ["Error", "TypeError", "RangeError", "OperationError", "InvalidStateError", "InvalidModificationError", "InvalidAccessError", "NotSupportedError", "SyntaxError", "AbortError"]})) {
|
|
27
|
+
if (allowed.includes(source[key])) media[key] = source[key];
|
|
28
|
+
}
|
|
26
29
|
if (Object.keys(media).length) result.media = media;
|
|
27
30
|
}
|
|
28
31
|
return result;
|
package/lib/matter-media.mjs
CHANGED
|
@@ -8,6 +8,7 @@ const integer = (value, fallback, min, max) => Number.isInteger(value) && value
|
|
|
8
8
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
9
9
|
const timer = (callback, ms) => { const value = setTimeout(callback, ms); value.unref(); return value; };
|
|
10
10
|
const isRtp = bytes => Buffer.isBuffer(bytes) && bytes.length >= 12 && bytes.length <= 2048 && (bytes[0] >> 6) === 2;
|
|
11
|
+
const exceptionTypes = new Set(['Error', 'TypeError', 'RangeError', 'OperationError', 'InvalidStateError', 'InvalidModificationError', 'InvalidAccessError', 'NotSupportedError', 'SyntaxError', 'AbortError']);
|
|
11
12
|
function localUrl(value, paths) {
|
|
12
13
|
try {
|
|
13
14
|
const url = new URL(value);
|
|
@@ -113,26 +114,45 @@ export class MatterMedia {
|
|
|
113
114
|
if (track.kind === 'audio') track.onReceiveRtp.subscribe(packet => this.receiveTalk(session, packet));
|
|
114
115
|
});
|
|
115
116
|
try {
|
|
116
|
-
|
|
117
|
+
// Install source codec constraints before applying an offer. With no local
|
|
118
|
+
// source werift chooses the first H264 MIME match, which can be a different
|
|
119
|
+
// profile from the encoded RTP source in a browser's multi-profile offer.
|
|
117
120
|
for (const [kind, enabled] of [['video', session.video], ['audio', session.audio]]) {
|
|
118
121
|
if (!enabled) continue;
|
|
122
|
+
session.negotiationStage = `replace_${kind}_track`;
|
|
119
123
|
const track = new MediaStreamTrack({ kind, codec: codecs[kind][0] }); session[`${kind}Track`] = track;
|
|
120
|
-
const existing = offer !== undefined ? peer.getTransceivers().find(t => t.kind === kind && !t.stopped) : undefined;
|
|
121
124
|
const direction = kind === 'audio' && this.config.enableTalkback !== false ? 'sendrecv' : 'sendonly';
|
|
122
|
-
|
|
123
|
-
else if (offer === undefined) session[`${kind}Sender`] = peer.addTransceiver(track, { direction }).sender;
|
|
124
|
-
else throw fault('media_not_offered');
|
|
125
|
+
session[`${kind}Sender`] = peer.addTransceiver(track, { direction }).sender;
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
+
if (offer !== undefined) {
|
|
128
|
+
session.negotiationStage = 'remote_description'; await peer.setRemoteDescription({ type: 'offer', sdp: offer });
|
|
129
|
+
for (const kind of ['video', 'audio']) {
|
|
130
|
+
if (session[kind] && !peer.getTransceivers().some(t => t.sender === session[`${kind}Sender`] && t.mid != null && !t.stopped && !t.rejected)) throw fault('media_not_offered');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
session.negotiationStage = offer !== undefined ? 'create_answer' : 'create_offer';
|
|
134
|
+
const description = offer !== undefined ? await peer.createAnswer() : await peer.createOffer();
|
|
135
|
+
session.negotiationStage = 'local_description'; await peer.setLocalDescription(description);
|
|
127
136
|
if (!this.alive(session)) throw fault('session_closed');
|
|
128
137
|
return offer !== undefined ? { answer: peer.localDescription.sdp } : { offer: peer.localDescription.sdp };
|
|
129
|
-
} catch (error) {
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const reason = error?.reason === 'media_not_offered' ? 'media_not_offered' : 'negotiation_failed';
|
|
140
|
+
this.negotiationFailure(session, error, reason);
|
|
141
|
+
await this.closeSession(id); throw fault(reason);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
negotiationFailure(session, error, reason = 'negotiation_failed') {
|
|
145
|
+
this.counters.failures++; this.counters.lastFailure = reason;
|
|
146
|
+
this.counters.lastFailureStage = session.negotiationStage || 'create_offer';
|
|
147
|
+
this.counters.lastFailureType = exceptionTypes.has(error?.name) ? error.name : 'Error';
|
|
130
148
|
}
|
|
131
149
|
getSession(id) { const session = this.session; if (!session || session.id !== id || session.closed) throw fault('unknown_session'); return session; }
|
|
132
150
|
async provideAnswer(id, value) {
|
|
133
151
|
const session = this.getSession(id); sdp(value);
|
|
134
152
|
if (session.peer.signalingState !== 'have-local-offer') throw fault('invalid_session_state');
|
|
135
|
-
|
|
153
|
+
session.negotiationStage = 'remote_answer';
|
|
154
|
+
try { await session.peer.setRemoteDescription({ type: 'answer', sdp: value }); }
|
|
155
|
+
catch (error) { this.negotiationFailure(session, error); await this.closeSession(id); throw fault('negotiation_failed'); }
|
|
136
156
|
}
|
|
137
157
|
async addCandidates(id, candidates) {
|
|
138
158
|
const session = this.getSession(id);
|
package/lib/platform.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export class MijiaRemoteCameraPlatform {
|
|
|
95
95
|
?.setCharacteristic(this.api.hap.Characteristic.Manufacturer, 'Xiaomi')
|
|
96
96
|
.setCharacteristic(this.api.hap.Characteristic.Model, label(selected.model || '米家远程摄像头'))
|
|
97
97
|
.setCharacteristic(this.api.hap.Characteristic.SerialNumber, `MiCam-${opaqueId}`)
|
|
98
|
-
.setCharacteristic(this.api.hap.Characteristic.FirmwareRevision, '0.2.
|
|
98
|
+
.setCharacteristic(this.api.hap.Characteristic.FirmwareRevision, '0.2.1');
|
|
99
99
|
this.delegate = this.delegateFactory({ api: this.api, log: this.log, runtime: this.runtime,
|
|
100
100
|
config: this.config, ffmpegPath: this.runtime.ffmpegPath });
|
|
101
101
|
accessory.configureController(this.delegate.controller);
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linanw/homebridge-mijia-remote-camera",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@linanw/homebridge-mijia-remote-camera",
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.1",
|
|
10
10
|
"cpu": [
|
|
11
11
|
"x64"
|
|
12
12
|
],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linanw/homebridge-mijia-remote-camera",
|
|
3
3
|
"displayName": "米家远程摄像头",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "Standalone Xiaomi remote camera plugin with HomeKit and experimental Matter camera streaming",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "linanw",
|