UNPKG

polyv-live-cli

Version:

CLI tool for managing PolyV live streaming services.

489 lines 21.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.StreamService = void 0; const axios_1 = __importDefault(require("axios")); const signature_1 = require("../utils/signature"); const errors_1 = require("../utils/errors"); class StreamService { constructor(authConfig, serviceConfig) { this.authConfig = authConfig; this.config = serviceConfig; this.httpClient = this.createHttpClient(); } async getStreamKey(request) { try { this.validateGetStreamKeyRequest(request); const timestamp = Date.now(); const signatureParams = { appId: this.authConfig.appId, timestamp, channelId: request.channelId }; const signatureResult = (0, signature_1.generateSignature)(signatureParams, { appSecret: this.authConfig.appSecret, debug: this.config.debug }); const url = '/live/v4/channel/basic/get'; const params = { appId: this.authConfig.appId, timestamp: signatureResult.timestamp, sign: signatureResult.signature, channelId: request.channelId }; if (this.config.debug) { console.log('[StreamService] Getting stream key with request:', request); console.log('[StreamService] API URL:', `${this.config.baseUrl}${url}`); console.log('[StreamService] Auth params:', params); } const response = await this.httpClient.get(url, { params }); if (response.data.code !== 200 || !response.data.success) { const errorMessage = response.data.error?.desc || 'Failed to get channel information'; throw new errors_1.PolyVAPIError(errorMessage, 'STREAM_API_ERROR', response.data.code, { channelId: request.channelId, apiStatus: response.data.status, requestId: response.data.requestId }); } if (!response.data.data) { throw new errors_1.PolyVAPIError('Channel information not available.', 'CHANNEL_NOT_FOUND', 400, { channelId: request.channelId }); } const channelData = response.data.data; if (!channelData.pushUrl || !channelData.pushSecret) { throw new errors_1.PolyVAPIError('Push URL and secret not available. This may not be a pure video channel or streaming is not configured.', 'STREAM_CREDENTIALS_NOT_AVAILABLE', 400, { channelId: request.channelId, streamType: channelData.streamType, template: channelData.template }); } return this.transformChannelDataToStreamCredentials(channelData, request.channelId); } catch (error) { if (error instanceof errors_1.PolyVError) { throw error; } if (axios_1.default.isAxiosError(error) || (error && typeof error === 'object' && ('response' in error || 'request' in error || 'config' in error))) { const apiError = this.transformAxiosError(error); throw apiError; } throw new errors_1.PolyVError('Failed to get stream key due to unexpected error', 'STREAM_GET_KEY_UNEXPECTED_ERROR', 500, { originalError: error instanceof Error ? error.message : String(error), channelId: request.channelId }); } } validateGetStreamKeyRequest(request) { if (!request.channelId || typeof request.channelId !== 'string' || request.channelId.trim() === '') { throw new errors_1.PolyVValidationError('Channel ID cannot be empty', 'channelId', request.channelId, 'required'); } } createHttpClient() { const client = axios_1.default.create({ baseURL: this.config.baseUrl, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'User-Agent': 'PolyV-CLI/3.1.0' } }); client.interceptors.request.use((config) => { if (this.config.debug) { console.log('[StreamService] Request:', { method: config.method, url: config.url, params: config.params }); } return config; }, (error) => { if (this.config.debug) { console.error('[StreamService] Request error:', error); } return Promise.reject(error); }); client.interceptors.response.use((response) => { if (this.config.debug) { console.log('[StreamService] Response:', { status: response.status, data: response.data }); } return response; }, (error) => { if (this.config.debug) { console.error('[StreamService] Response error:', error); } return Promise.reject(error); }); return client; } transformChannelDataToStreamCredentials(channelData, channelId) { return { channelId, rtmpUrl: channelData.pushUrl || '', streamKey: channelData.pushSecret || '', deployAddress: '', inAddress: '', metrics: { fps: 0, lfr: 0, bandwidth: 0 } }; } transformAxiosError(error) { if (error.response) { const data = error.response.data; return new errors_1.PolyVAPIError(data?.message || 'API request failed', 'STREAM_HTTP_ERROR', data?.code || error.response.status, { httpStatus: error.response.status, url: error.config?.url, method: error.config?.method, apiStatus: data?.status || 'error' }); } else if (error.request) { return new errors_1.PolyVAPIError('No response received from API server', 'STREAM_NETWORK_ERROR', 0, { timeout: error.code === 'ECONNABORTED', url: error.config?.url }); } else { return new errors_1.PolyVAPIError('Request setup failed', 'STREAM_REQUEST_ERROR', 0, { message: error.message, url: error.config?.url }); } } async startStream(request) { try { this.validateStartStreamRequest(request); const timestamp = Date.now(); const signatureParams = { appId: this.authConfig.appId, timestamp, ...(this.authConfig.userId && { userId: this.authConfig.userId }) }; const signatureResult = (0, signature_1.generateSignature)(signatureParams, { appSecret: this.authConfig.appSecret, debug: this.config.debug }); const url = `/live/v2/channels/${request.channelId}/live`; const data = { appId: this.authConfig.appId, timestamp: signatureResult.timestamp, sign: signatureResult.signature, ...(this.authConfig.userId && { userId: this.authConfig.userId }) }; if (this.config.debug) { console.log('[StreamService] Starting stream with request:', request); console.log('[StreamService] API URL:', `${this.config.baseUrl}${url}`); console.log('[StreamService] Request data:', data); } const response = await this.httpClient.post(url, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (response.data.code !== 200) { throw new errors_1.PolyVAPIError(response.data.message || 'Failed to start stream', 'STREAM_START_API_ERROR', response.data.code, { channelId: request.channelId, apiStatus: response.data.status }); } return response.data; } catch (error) { if (error instanceof errors_1.PolyVError) { throw error; } if (axios_1.default.isAxiosError(error) || (error && typeof error === 'object' && ('response' in error || 'request' in error || 'config' in error))) { const apiError = this.transformAxiosError(error); throw apiError; } throw new errors_1.PolyVError('Failed to start stream due to unexpected error', 'STREAM_START_UNEXPECTED_ERROR', 500, { originalError: error instanceof Error ? error.message : String(error), channelId: request.channelId }); } } validateStartStreamRequest(request) { if (!request.channelId || typeof request.channelId !== 'string' || request.channelId.trim() === '') { throw new errors_1.PolyVValidationError('Channel ID cannot be empty', 'channelId', request.channelId, 'required'); } } async stopStream(request) { try { this.validateStopStreamRequest(request); const timestamp = Date.now(); const signatureParams = { appId: this.authConfig.appId, timestamp, ...(this.authConfig.userId && { userId: this.authConfig.userId }) }; const signatureResult = (0, signature_1.generateSignature)(signatureParams, { appSecret: this.authConfig.appSecret, debug: this.config.debug }); const url = `/live/v2/channels/${request.channelId}/end`; const data = { appId: this.authConfig.appId, timestamp: signatureResult.timestamp, sign: signatureResult.signature, ...(this.authConfig.userId && { userId: this.authConfig.userId }) }; if (this.config.debug) { console.log('[StreamService] Stopping stream with request:', request); console.log('[StreamService] API URL:', `${this.config.baseUrl}${url}`); console.log('[StreamService] Request data:', data); } const response = await this.httpClient.post(url, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }); if (response.data.code !== 200) { throw new errors_1.PolyVAPIError(response.data.message || 'Failed to stop stream', 'STREAM_STOP_API_ERROR', response.data.code, { channelId: request.channelId, apiStatus: response.data.status }); } return response.data; } catch (error) { if (error instanceof errors_1.PolyVError) { throw error; } if (axios_1.default.isAxiosError(error) || (error && typeof error === 'object' && ('response' in error || 'request' in error || 'config' in error))) { const apiError = this.transformAxiosError(error); throw apiError; } throw new errors_1.PolyVError('Failed to stop stream due to unexpected error', 'STREAM_STOP_UNEXPECTED_ERROR', 500, { originalError: error instanceof Error ? error.message : String(error), channelId: request.channelId }); } } validateStopStreamRequest(request) { if (!request.channelId || typeof request.channelId !== 'string' || request.channelId.trim() === '') { throw new errors_1.PolyVValidationError('Channel ID cannot be empty', 'channelId', request.channelId, 'required'); } } async getStreamStatus(request) { try { this.validateStreamStatusRequest(request); let streamMonitorData = null; let streamInfoError; try { streamMonitorData = await this.getStreamInfo(request.channelId); } catch (error) { if (error instanceof errors_1.PolyVAPIError) { streamInfoError = { code: error.code || 'STREAM_INFO_ERROR', message: error.message }; } } let channelStatus = 'unknown'; let statusText = 'Unknown'; let isLive = false; let channelError; try { const channelInfo = await this.getChannelBasicInfo(request.channelId); if (channelInfo && channelInfo.watchStatus) { switch (channelInfo.watchStatus) { case 'live': channelStatus = 'live'; statusText = 'Live'; isLive = true; break; case 'waiting': case 'unStart': channelStatus = 'waiting'; statusText = 'Waiting'; break; case 'end': case 'playback': channelStatus = 'stopped'; statusText = 'Stopped'; break; case 'banpush': channelStatus = 'error'; statusText = 'Push Banned'; break; default: channelStatus = 'unknown'; statusText = channelInfo.watchStatus || 'Unknown'; } } } catch (error) { channelStatus = 'error'; statusText = 'Channel Error'; if (error instanceof errors_1.PolyVAPIError) { channelError = { code: error.code || 'CHANNEL_ERROR', message: error.message }; } } let duration; let durationText; if (isLive && streamMonitorData && streamMonitorData.retrievedAt) { duration = Date.now() - streamMonitorData.retrievedAt.getTime(); durationText = this.formatDuration(duration); } let metrics; if (streamMonitorData) { const fpsValue = parseFloat(streamMonitorData.fps) || 0; const lfrValue = parseFloat(streamMonitorData.lfr || '0') || 0; const bandwidthValue = parseFloat(streamMonitorData.inBandWidth) || 0; metrics = { fps: fpsValue, lfr: lfrValue, bandwidth: bandwidthValue, bandwidthText: this.formatBandwidth(bandwidthValue) }; } let network; if (streamMonitorData) { network = { ...(streamMonitorData.deployAddress && { deployAddress: streamMonitorData.deployAddress }), ...(streamMonitorData.inAddress && { inAddress: streamMonitorData.inAddress }), ...(streamMonitorData.streamName && { streamName: streamMonitorData.streamName }) }; } let finalError; if (channelStatus === 'waiting' && streamInfoError) { finalError = streamInfoError; } else if (channelStatus === 'error') { finalError = channelError || streamInfoError; } const statusInfo = { channelId: request.channelId, status: channelStatus, statusText, isLive, ...(duration !== undefined && { duration }), ...(durationText !== undefined && { durationText }), ...(metrics !== undefined && { metrics }), ...(network !== undefined && { network }), lastUpdated: new Date(), ...(finalError !== undefined && { error: finalError }) }; return statusInfo; } catch (error) { if (error instanceof errors_1.PolyVError) { throw error; } throw new errors_1.PolyVError('Failed to get stream status due to unexpected error', 'STREAM_STATUS_UNEXPECTED_ERROR', 500, { originalError: error instanceof Error ? error.message : String(error), channelId: request.channelId }); } } async getStreamInfo(channelId) { const timestamp = Date.now(); const signatureParams = { appId: this.authConfig.appId, timestamp, channelId }; const signatureResult = (0, signature_1.generateSignature)(signatureParams, { appSecret: this.authConfig.appSecret, debug: this.config.debug }); const url = '/live/v3/channel/monitor/get-stream-info'; const params = { appId: this.authConfig.appId, timestamp: signatureResult.timestamp, sign: signatureResult.signature, channelId }; if (this.config.debug) { console.log('[StreamService] Getting stream info with channelId:', channelId); console.log('[StreamService] API URL:', `${this.config.baseUrl}${url}`); console.log('[StreamService] Auth params:', params); } const response = await this.httpClient.get(url, { params }); if (response.data.code !== 200) { throw new errors_1.PolyVAPIError(response.data.message || 'Failed to get stream information', 'STREAM_INFO_API_ERROR', response.data.code, { channelId, apiStatus: response.data.status }); } const data = response.data.data; if (!data) { throw new errors_1.PolyVAPIError('Stream information not available', 'STREAM_INFO_NOT_AVAILABLE', 400, { channelId }); } return { deployAddress: data.deployAddress, inAddress: data.inAddress, streamName: data.streamName || '', fps: data.fps || '0', lfr: data.lfr, inBandWidth: data.inBandWidth || '0', retrievedAt: new Date(), channelId }; } async getChannelBasicInfo(channelId) { const timestamp = Date.now(); const signatureParams = { appId: this.authConfig.appId, timestamp, channelId }; const signatureResult = (0, signature_1.generateSignature)(signatureParams, { appSecret: this.authConfig.appSecret, debug: this.config.debug }); const url = '/live/v4/channel/basic/get'; const params = { appId: this.authConfig.appId, timestamp: signatureResult.timestamp, sign: signatureResult.signature, channelId }; const response = await this.httpClient.get(url, { params }); if (response.data.code !== 200 || !response.data.success) { throw new errors_1.PolyVAPIError('Failed to get channel information', 'CHANNEL_INFO_API_ERROR', response.data.code, { channelId }); } return response.data.data; } validateStreamStatusRequest(request) { if (!request.channelId || typeof request.channelId !== 'string' || request.channelId.trim() === '') { throw new errors_1.PolyVValidationError('Channel ID cannot be empty', 'channelId', request.channelId, 'required'); } } formatDuration(duration) { const seconds = Math.floor(duration / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) { return `${hours}h ${minutes % 60}m ${seconds % 60}s`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } formatBandwidth(bandwidth) { if (bandwidth === 0) return '0 bps'; const units = ['bps', 'Kbps', 'Mbps', 'Gbps']; let unitIndex = 0; let value = bandwidth; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex++; } return `${value.toFixed(2)} ${units[unitIndex]}`; } } exports.StreamService = StreamService; //# sourceMappingURL=stream.service.js.map