[mirotalksfu] - refactoring
هذا الالتزام موجود في:
@@ -93,7 +93,7 @@ module.exports = class Peer {
|
|||||||
return this.producers.get(producer_id);
|
return this.producers.get(producer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProducer(producerTransportId, rtpParameters, kind, type) {
|
async createProducer(producerTransportId, producer_rtpParameters, producer_kind, producer_type) {
|
||||||
try {
|
try {
|
||||||
if (!producerTransportId) {
|
if (!producerTransportId) {
|
||||||
throw new Error('Invalid producer transport ID');
|
throw new Error('Invalid producer transport ID');
|
||||||
@@ -105,41 +105,40 @@ module.exports = class Peer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const producer = await producerTransport.produce({
|
const producer = await producerTransport.produce({
|
||||||
kind,
|
kind: producer_kind,
|
||||||
rtpParameters,
|
rtpParameters: producer_rtpParameters,
|
||||||
});
|
});
|
||||||
|
|
||||||
producer.appData.mediaType = type;
|
const { id, appData, type, rtpParameters } = producer;
|
||||||
|
|
||||||
this.producers.set(producer.id, producer);
|
appData.mediaType = producer_type;
|
||||||
|
|
||||||
const producerType = producer.type;
|
this.producers.set(id, producer);
|
||||||
|
|
||||||
if (['simulcast', 'svc'].includes(producerType)) {
|
if (['simulcast', 'svc'].includes(type)) {
|
||||||
const { scalabilityMode } = producer.rtpParameters.encodings[0];
|
const { scalabilityMode } = rtpParameters.encodings[0];
|
||||||
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
|
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
|
||||||
const temporalLayer = parseInt(scalabilityMode.substring(3, 4)); // 1/2/3
|
const temporalLayer = parseInt(scalabilityMode.substring(3, 4)); // 1/2/3
|
||||||
log.debug(`Producer [${producerType}] created with ID ${producer.id}`, {
|
log.debug(`Producer [${type}] created with ID ${id}`, {
|
||||||
scalabilityMode,
|
scalabilityMode,
|
||||||
spatialLayer,
|
spatialLayer,
|
||||||
temporalLayer,
|
temporalLayer,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.debug(`Producer of type ${producerType} created with ID ${producer.id}`);
|
log.debug(`Producer of type ${type} created with ID ${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
producer.on('transportclose', () => {
|
producer.on('transportclose', () => {
|
||||||
log.debug('Producer transport closed', {
|
log.debug('Producer transport closed', {
|
||||||
peer_name: this.peer_info?.peer_name,
|
peer_name: this.peer_info?.peer_name,
|
||||||
producer_id: producer.id,
|
producer_id: id,
|
||||||
});
|
});
|
||||||
producer.close();
|
this.closeProducer(id);
|
||||||
this.producers.delete(producer.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return producer;
|
return producer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error creating producer', error);
|
log.error('Error creating producer', error.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,48 +172,48 @@ module.exports = class Peer {
|
|||||||
paused: false,
|
paused: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const consumerType = consumer.type;
|
const { id, type, kind, rtpParameters, producerPaused } = consumer;
|
||||||
|
|
||||||
if (['simulcast', 'svc'].includes(consumerType)) {
|
if (['simulcast', 'svc'].includes(type)) {
|
||||||
const { scalabilityMode } = consumer.rtpParameters.encodings[0];
|
const { scalabilityMode } = rtpParameters.encodings[0];
|
||||||
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
|
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
|
||||||
const temporalLayer = parseInt(scalabilityMode.substring(3, 4)); // 1/2/3
|
const temporalLayer = parseInt(scalabilityMode.substring(3, 4)); // 1/2/3
|
||||||
await consumer.setPreferredLayers({
|
await consumer.setPreferredLayers({
|
||||||
spatialLayer: spatialLayer,
|
spatialLayer: spatialLayer,
|
||||||
temporalLayer: temporalLayer,
|
temporalLayer: temporalLayer,
|
||||||
});
|
});
|
||||||
log.debug(`Consumer [${consumerType}] ----->`, {
|
log.debug(`Consumer [${type}] ----->`, {
|
||||||
scalabilityMode,
|
scalabilityMode,
|
||||||
spatialLayer,
|
spatialLayer,
|
||||||
temporalLayer,
|
temporalLayer,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.debug('Consumer ----->', { type: consumerType });
|
log.debug('Consumer ----->', { type: type });
|
||||||
}
|
}
|
||||||
|
|
||||||
consumer.on('transportclose', () => {
|
consumer.on('transportclose', () => {
|
||||||
log.debug('Consumer transport close', {
|
log.debug('Consumer transport close', {
|
||||||
peer_name: this.peer_info?.peer_name,
|
peer_name: this.peer_info?.peer_name,
|
||||||
consumer_id: consumer.id,
|
consumer_id: id,
|
||||||
});
|
});
|
||||||
this.removeConsumer(consumer.id);
|
this.removeConsumer(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.consumers.set(consumer.id, consumer);
|
this.consumers.set(id, consumer);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
consumer,
|
consumer,
|
||||||
params: {
|
params: {
|
||||||
producerId: producer_id,
|
producerId: producer_id,
|
||||||
id: consumer.id,
|
id: id,
|
||||||
kind: consumer.kind,
|
kind: kind,
|
||||||
rtpParameters: consumer.rtpParameters,
|
rtpParameters: rtpParameters,
|
||||||
type: consumer.type,
|
type: type,
|
||||||
producerPaused: consumer.producerPaused,
|
producerPaused: producerPaused,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error creating consumer', error);
|
log.error('Error creating consumer', error.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ module.exports = class Room {
|
|||||||
audioVolume: audioVolume,
|
audioVolume: audioVolume,
|
||||||
};
|
};
|
||||||
// Uncomment the following line for debugging
|
// Uncomment the following line for debugging
|
||||||
// log.debug('Sending audio volume:', data);
|
// log.debug('Sending audio volume', data);
|
||||||
this.broadCast(0, 'audioVolume', data);
|
this.broadCast(0, 'audioVolume', data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -110,7 +110,7 @@ module.exports = class Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error sending active speaker volume', error);
|
log.error('Error sending active speaker volume', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,10 +201,11 @@ module.exports = class Room {
|
|||||||
const producerList = [];
|
const producerList = [];
|
||||||
this.peers.forEach((peer) => {
|
this.peers.forEach((peer) => {
|
||||||
peer.producers.forEach((producer) => {
|
peer.producers.forEach((producer) => {
|
||||||
|
const { peer_name, peer_info } = peer;
|
||||||
producerList.push({
|
producerList.push({
|
||||||
producer_id: producer.id,
|
producer_id: producer.id,
|
||||||
peer_name: peer.peer_name,
|
peer_name: peer_name,
|
||||||
peer_info: peer.peer_info,
|
peer_info: peer_info,
|
||||||
type: producer.appData.mediaType,
|
type: producer.appData.mediaType,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -224,7 +225,7 @@ module.exports = class Room {
|
|||||||
|
|
||||||
await this.peers.get(socket_id).connectTransport(transport_id, dtlsParameters);
|
await this.peers.get(socket_id).connectTransport(transport_id, dtlsParameters);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error connecting peer transport', error);
|
log.error('Error connecting peer transport', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,34 +254,44 @@ module.exports = class Room {
|
|||||||
initialAvailableOutgoingBitrate,
|
initialAvailableOutgoingBitrate,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { id, iceParameters, iceCandidates, dtlsParameters } = transport;
|
||||||
|
|
||||||
if (maxIncomingBitrate) {
|
if (maxIncomingBitrate) {
|
||||||
await transport.setMaxIncomingBitrate(maxIncomingBitrate);
|
try {
|
||||||
|
await transport.setMaxIncomingBitrate(maxIncomingBitrate);
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Transport setMaxIncomingBitrate error', error.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const peer = this.peers.get(socket_id);
|
||||||
|
|
||||||
|
const { peer_name } = peer;
|
||||||
|
|
||||||
transport.on('dtlsstatechange', (dtlsState) => {
|
transport.on('dtlsstatechange', (dtlsState) => {
|
||||||
if (dtlsState === 'closed') {
|
if (dtlsState === 'closed') {
|
||||||
log.debug('Transport closed', { peer_name: this.peers.get(socket_id)?.peer_name });
|
log.debug('Transport closed', { peer_name: peer_name });
|
||||||
transport.close();
|
transport.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
transport.on('close', () => {
|
transport.on('close', () => {
|
||||||
log.debug('Transport closed', { peer_name: this.peers.get(socket_id)?.peer_name });
|
log.debug('Transport closed', { peer_name: peer_name });
|
||||||
});
|
});
|
||||||
|
|
||||||
log.debug('Adding transport', { transportId: transport.id });
|
log.debug('Adding transport', { transportId: id });
|
||||||
this.peers.get(socket_id)?.addTransport(transport);
|
peer.addTransport(transport);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
id: transport.id,
|
id: id,
|
||||||
iceParameters: transport.iceParameters,
|
iceParameters: iceParameters,
|
||||||
iceCandidates: transport.iceCandidates,
|
iceCandidates: iceCandidates,
|
||||||
dtlsParameters: transport.dtlsParameters,
|
dtlsParameters: dtlsParameters,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error creating WebRTC transport', error);
|
log.error('Error creating WebRTC transport', error.message);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,31 +306,35 @@ module.exports = class Room {
|
|||||||
throw new Error('Invalid input parameters');
|
throw new Error('Invalid input parameters');
|
||||||
}
|
}
|
||||||
|
|
||||||
const producer = await this.peers
|
if (!this.peers.has(socket_id)) {
|
||||||
.get(socket_id)
|
throw new Error(`Invalid socket ID: ${socket_id}`);
|
||||||
?.createProducer(producerTransportId, rtpParameters, kind, type);
|
}
|
||||||
|
|
||||||
|
const peer = this.peers.get(socket_id);
|
||||||
|
|
||||||
|
const producer = await peer.createProducer(producerTransportId, rtpParameters, kind, type);
|
||||||
if (!producer) {
|
if (!producer) {
|
||||||
throw new Error('Failed to create producer');
|
throw new Error('Failed to create producer');
|
||||||
}
|
}
|
||||||
|
|
||||||
const peer = this.peers.get(socket_id);
|
const { id } = producer;
|
||||||
const peerName = peer?.peer_name;
|
|
||||||
const peerInfo = peer?.peer_info;
|
const { peer_name, peer_info } = peer;
|
||||||
|
|
||||||
this.broadCast(socket_id, 'newProducers', [
|
this.broadCast(socket_id, 'newProducers', [
|
||||||
{
|
{
|
||||||
producer_id: producer.id,
|
producer_id: id,
|
||||||
producer_socket_id: socket_id,
|
producer_socket_id: socket_id,
|
||||||
peer_name: peerName,
|
peer_name: peer_name,
|
||||||
peer_info: peerInfo,
|
peer_info: peer_info,
|
||||||
type: type,
|
type: type,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return producer.id;
|
return id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error producing', error);
|
console.error('Error producing', error.message);
|
||||||
throw error;
|
throw error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,12 +357,15 @@ module.exports = class Room {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const peer = this.peers.get(socket_id);
|
if (!this.peers.has(socket_id)) {
|
||||||
if (!peer) {
|
log.warn('Peer not found for socket ID', socket_id);
|
||||||
log.warn('Peer not found for socket ID:', socket_id);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const peer = this.peers.get(socket_id);
|
||||||
|
|
||||||
|
const { peer_name } = peer;
|
||||||
|
|
||||||
const result = await peer.createConsumer(consumer_transport_id, producer_id, rtpCapabilities);
|
const result = await peer.createConsumer(consumer_transport_id, producer_id, rtpCapabilities);
|
||||||
|
|
||||||
if (!result || !result.consumer || !result.params) {
|
if (!result || !result.consumer || !result.params) {
|
||||||
@@ -357,17 +375,19 @@ module.exports = class Room {
|
|||||||
|
|
||||||
const { consumer, params } = result;
|
const { consumer, params } = result;
|
||||||
|
|
||||||
|
const { id, kind } = consumer;
|
||||||
|
|
||||||
consumer.on('producerclose', () => {
|
consumer.on('producerclose', () => {
|
||||||
log.debug('Consumer closed due to producerclose event', {
|
log.debug('Consumer closed due to producerclose event', {
|
||||||
peer_name: this.peers.get(socket_id)?.peer_name,
|
peer_name: peer_name,
|
||||||
consumer_id: consumer.id,
|
consumer_id: id,
|
||||||
});
|
});
|
||||||
this.peers.get(socket_id)?.removeConsumer(consumer.id);
|
peer.removeConsumer(id);
|
||||||
|
|
||||||
// Tell client consumer is dead
|
// Tell client consumer is dead
|
||||||
this.io.to(socket_id).emit('consumerClosed', {
|
this.io.to(socket_id).emit('consumerClosed', {
|
||||||
consumer_id: consumer.id,
|
consumer_id: id,
|
||||||
consumer_kind: consumer.kind,
|
consumer_kind: kind,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -385,7 +405,7 @@ module.exports = class Room {
|
|||||||
}
|
}
|
||||||
this.peers.get(socket_id).closeProducer(producer_id);
|
this.peers.get(socket_id).closeProducer(producer_id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error('Error closing producer:', error);
|
log.error('Error closing producer', error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ dependencies: {
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.3.85
|
* @version 1.3.86
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mirotalksfu",
|
"name": "mirotalksfu",
|
||||||
"version": "1.3.85",
|
"version": "1.3.86",
|
||||||
"description": "WebRTC SFU browser-based video calls",
|
"description": "WebRTC SFU browser-based video calls",
|
||||||
"main": "Server.js",
|
"main": "Server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ if (location.href.substr(0, 5) !== 'https') location.href = 'https' + location.h
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.3.85
|
* @version 1.3.86
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -1752,9 +1752,9 @@ async function changeCamera(deviceId) {
|
|||||||
checkInitConfig();
|
checkInitConfig();
|
||||||
handleCameraMirror(initVideo);
|
handleCameraMirror(initVideo);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
console.error('[Error] changeCamera', err);
|
console.error('[Error] changeCamera', error);
|
||||||
userLog('error', 'Error while swapping camera' + err, 'top-end');
|
userLog('error', 'Error while swapping camera' + error.message, 'top-end');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1781,8 +1781,8 @@ async function toggleScreenSharing() {
|
|||||||
disable(initVideoButton, true);
|
disable(initVideoButton, true);
|
||||||
disable(initAudioVideoButton, true);
|
disable(initAudioVideoButton, true);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((error) => {
|
||||||
console.error('[Error] toggleScreenSharing', err);
|
console.error('[Error] toggleScreenSharing', error);
|
||||||
joinRoomWithScreen = false;
|
joinRoomWithScreen = false;
|
||||||
return checkInitVideo(isVideoAllowed);
|
return checkInitVideo(isVideoAllowed);
|
||||||
});
|
});
|
||||||
@@ -2246,7 +2246,7 @@ function handleRoomEmojiPicker() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function sendEmojiToRoom(data) {
|
function sendEmojiToRoom(data) {
|
||||||
console.log('Selected Emoji:', data.native);
|
console.log('Selected Emoji', data.native);
|
||||||
const cmd = {
|
const cmd = {
|
||||||
type: 'roomEmoji',
|
type: 'roomEmoji',
|
||||||
peer_name: peer_name,
|
peer_name: peer_name,
|
||||||
@@ -2995,8 +2995,8 @@ async function loadPDF(pdfData, pages) {
|
|||||||
);
|
);
|
||||||
return canvases.filter((canvas) => canvas !== null);
|
return canvases.filter((canvas) => canvas !== null);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading PDF:', error);
|
console.error('Error loading PDF', error.message);
|
||||||
throw error;
|
throw error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3013,8 +3013,8 @@ async function pdfToImage(pdfData, canvas) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error converting PDF to images:', error);
|
console.error('Error converting PDF to images', error.message);
|
||||||
throw error;
|
throw error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
* @license For commercial or closed source, contact us at license.mirotalk@gmail.com or purchase directly via CodeCanyon
|
||||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-sfu-webrtc-realtime-video-conferences/40769970
|
||||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||||
* @version 1.3.85
|
* @version 1.3.86
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -2421,7 +2421,7 @@ class RoomClient {
|
|||||||
refreshLsDevices();
|
refreshLsDevices();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let error = `Browser seems doesn't support output device selection.`;
|
const error = `Browser seems doesn't support output device selection.`;
|
||||||
console.warn(error);
|
console.warn(error);
|
||||||
this.userLog('error', error, 'top-end', 6000);
|
this.userLog('error', error, 'top-end', 6000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ if (statsData) {
|
|||||||
window.sessionStorage.setItem(statsDataKey, JSON.stringify(data));
|
window.sessionStorage.setItem(statsDataKey, JSON.stringify(data));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Stats fetch error:', error);
|
console.error('Stats fetch error', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ function resizeVideoMedia() {
|
|||||||
let optional = isHideMeActive && videoMediaContainer.childElementCount <= 2 ? 1 : 0;
|
let optional = isHideMeActive && videoMediaContainer.childElementCount <= 2 ? 1 : 0;
|
||||||
let isOneVideoElement = videoMediaContainer.childElementCount - optional == 1 ? true : false;
|
let isOneVideoElement = videoMediaContainer.childElementCount - optional == 1 ? true : false;
|
||||||
|
|
||||||
// console.log('videoMediaContainer.childElementCount:', {
|
// console.log('videoMediaContainer.childElementCount', {
|
||||||
// isOneVideoElement: isOneVideoElement,
|
// isOneVideoElement: isOneVideoElement,
|
||||||
// children: videoMediaContainer.childElementCount,
|
// children: videoMediaContainer.childElementCount,
|
||||||
// optional: optional,
|
// optional: optional,
|
||||||
|
|||||||
المرجع في مشكلة جديدة
حظر مستخدم