polyv-live-cli
Version:
CLI tool for managing PolyV live streaming services.
489 lines • 21.3 kB
JavaScript
"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