async startRtmpStream(request: RtmpStreamRequest): Promise<string> {
const { packageName, rtmpUrl, video, audio, stream: streamOptions } = request;
// Validation
if (!this.userSession.appManager.isAppRunning(packageName)) {
throw new Error(`App ${packageName} is not running`);
}
if (!rtmpUrl || (!rtmpUrl.startsWith('rtmp://') && !rtmpUrl.startsWith('rtmps://'))) {
throw new Error('Invalid RTMP URL');
}
if (!this.userSession.websocket?.readyState === WebSocket.OPEN) {
throw new Error('Glasses WebSocket not connected');
}
// Check for managed stream conflicts
if (this.userSession.managedStreamingExtension.checkUnmanagedStreamConflict(this.userSession.userId)) {
throw new Error('Cannot start unmanaged stream - managed stream already active');
}
// Generate short ID for BLE efficiency
const streamId = `s${Date.now().toString(36).slice(-6)}${Math.random().toString(36).slice(2, 6)}`;
// Stop existing streams for this app
this.stopStreamsByPackageName(packageName);
// Create stream info
const streamInfo: SessionStreamInfo = {
streamId,
packageName,
rtmpUrl,
status: 'initializing',
startTime: now,
lastKeepAlive: now,
pendingAcks: new Map(),
missedAcks: 0,
options: { video, audio, stream: streamOptions }
};
this.activeSessionStreams.set(streamId, streamInfo);
this.scheduleKeepAlive(streamId);
// Send start command
const startMessage: StartRtmpStream = {
type: CloudToGlassesMessageType.START_RTMP_STREAM,
sessionId: this.userSession.sessionId,
rtmpUrl,
appId: packageName,
streamId,
video: video || {},
audio: audio || {},
stream: streamOptions || {},
timestamp: now
};
this.userSession.websocket.send(JSON.stringify(startMessage));
return streamId;
}