/* Copyright (C) 2015 Borsato Ivano The JavaScript code in this page is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License (GNU GPL) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. The code is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU GPL for more details. */ 'use strict'; import GObject from 'gi://GObject'; import GLib from 'gi://GLib'; import Gst from 'gi://Gst?version=1.0'; import * as Lib from './convenience.js'; export const HelperWebcam = GObject.registerClass({ GTypeName: 'EasyScreenCast_HelperWebcam', }, class HelperWebcam extends GObject.Object { /** * Create a device monitor inputvideo * * @param {string} unspecifiedWebcamText localized text for "Unspecified Webcam" */ constructor(unspecifiedWebcamText) { super(); this._unspecified_webcam_text = unspecifiedWebcamText; Lib.TalkativeLog('-@-init webcam'); var [result, _] = Gst.init_check(null); Lib.TalkativeLog(`-@-gstreamer init result: ${result}`); if (!result) { Lib.TalkativeLog('-@-gstreamer init failed'); throw new Error('GStreamer init failed'); } // get gstreamer lib version var [M, m, micro, nano] = Gst.version(); Lib.TalkativeLog( `-@-gstreamer version: ${M}.${m}.${micro}.${nano}` ); if (M === 1 && m >= 8) { // gstreamer version equal or higher 1.8 this.deviceMonitor = new Gst.DeviceMonitor({ show_all: true, }); } else { // gstreamer version lower 1.8 this.deviceMonitor = new Gst.DeviceMonitor(); } // get gstreamer plugin avaiable let registry = new Gst.Registry(); let listPI = registry.get_plugin_list(); Lib.TalkativeLog(`-@-plugin list: ${listPI.length}`); for (var ind in listPI) { Lib.TalkativeLog( `-@-plugin name: ${ listPI[ind].get_name() } Pfilename: ${ listPI[ind].get_filename() } Pdesc: ${ listPI[ind].get_description() } Pversion: ${ listPI[ind].get_version() } Pload: ${ listPI[ind].is_loaded()}` ); } // create device monitor if (this.deviceMonitor !== null && this.deviceMonitor !== undefined) { Lib.TalkativeLog('-@-device monitor created'); this.dmBus = this.deviceMonitor.get_bus(); if (this.dmBus !== null && this.dmBus !== undefined) { Lib.TalkativeLog('-@-dbus created'); this.dmBus.add_watch(GLib.PRIORITY_DEFAULT, this._getMsg.bind(this)); let caps = Gst.Caps.new_empty_simple('video/x-raw'); this.deviceMonitor.add_filter('Video/Source', caps); this.startMonitor(); // update device and caps this.refreshAllInputVideo(); } else { Lib.TalkativeLog('-@-ERROR dbus creation'); } } else { Lib.TalkativeLog('-@-ERROR device monitor creation'); } } /** * Callback for the DeviceMonitor watcher * * @param {Gst.Bus} bus the DeviceMonitor Bus of gstreamer * @param {Gst.Message} message the message * @returns {boolean} */ _getMsg(bus, message) { Lib.TalkativeLog('-@-event getmsg'); switch (message.type) { case Gst.MessageType.DEVICE_ADDED: Lib.TalkativeLog('Device added'); // update device and caps this.refreshAllInputVideo(); break; case Gst.MessageType.DEVICE_REMOVED: Lib.TalkativeLog('Device removed'); // update device and caps this.refreshAllInputVideo(); break; default: Lib.TalkativeLog('Device UNK'); break; } return GLib.SOURCE_CONTINUE; } /** * refresh all devices info */ refreshAllInputVideo() { Lib.TalkativeLog('-@-refresh all video input'); this._listDevices = this.getDevicesIV(); // compose devices array this._listCaps = []; for (var index in this._listDevices) { this._listCaps[index] = this.getCapsForIV(this._listDevices[index].caps); Lib.TalkativeLog(`-@-webcam /dev/video${index} name: ${this._listDevices[index].display_name}`); Lib.TalkativeLog(`-@-caps available: ${this._listCaps[index].length}`); Lib.TalkativeLog(`-@-ListCaps[${index}]: ${this._listCaps[index]}`); } } /** * get caps from device. * A single capability might look like: video/x-raw, format=(string)YUY2, width=(int)640, height=(int)480, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction)30/1 * This encodes a single capability (fixed), but there might also be capabilities which represent options, e.g. * video/x-raw, format=(string)YUY2, width=(int)640, height=(int)480, pixel-aspect-ratio=(fraction)1/1, framerate=(fraction) { 30/1, 25/1, 20/1 }. *
* The code here will take always the first option and unroll the options for framerate. * * @param {Gst.Caps} tmpCaps capabilities of a device * @returns {string[]} */ getCapsForIV(tmpCaps) { Lib.TalkativeLog('-@-get all caps from a input video'); Lib.TalkativeLog(`-@-caps available before filtering for video/x-raw: ${tmpCaps.get_size()}`); let cleanCaps = []; for (let i = 0; i < tmpCaps.get_size(); i++) { let capsStructure = tmpCaps.get_structure(i); // only consider "video/x-raw" if (capsStructure.get_name() === 'video/x-raw') { Lib.TalkativeLog(`-@-cap : ${i} : original : ${capsStructure.to_string()}`); let tmpStr = 'video/x-raw'; let result, number, fraction; result = capsStructure.get_string('format'); if (result !== null) tmpStr += `, format=(string)${result}`; [result, number] = capsStructure.get_int('width'); if (result === true) tmpStr += `, width=(int)${number}`; [result, number] = capsStructure.get_int('height'); if (result === true) tmpStr += `, height=(int)${number}`; [result, number, fraction] = capsStructure.get_fraction('pixel-aspect-ratio'); if (result === true) tmpStr += `, pixel-aspect-ratio=(fraction)${number}/${fraction}`; if (capsStructure.has_field('framerate')) { [result, number, fraction] = capsStructure.get_fraction('framerate'); if (result === true) { // a single framerate this._addAndLogCapability(cleanCaps, i, `${tmpStr}, framerate=(fraction)${number}/${fraction}`); } else { // multiple framerates // unfortunately GstValueList is not supported in this gjs-binding // "Error: Don't know how to convert GType GstValueList to JavaScript object" // -> capsStructure.get_value('framerate') <- won't work // -> capsStructure.get_list('framerate') <- only returns the numerator of the fraction // // therefore manually parsing the framerate values from the string representation let framerates = capsStructure.to_string(); framerates = framerates.substring(framerates.indexOf('framerate=(fraction){') + 21); framerates = framerates.substring(0, framerates.indexOf('}')); framerates.split(',').forEach(element => { let [numerator, denominator] = element.split('/', 2); this._addAndLogCapability(cleanCaps, i, `${tmpStr}, framerate=(fraction)${numerator.trim()}/${denominator.trim()}`); }); } } else { // no framerate at all this._addAndLogCapability(cleanCaps, i, tmpStr); } } else { Lib.TalkativeLog(`-@-cap : ${i} : skipped : ${capsStructure.to_string()}`); } } return cleanCaps; } /** * Adds the capability str to the array caps, if it is not already there. Avoids duplicates. * * @param {Array} caps the list of capabilities * @param {int} originalIndex index of the original capabilities list from the device * @param {string} str the capability string to add */ _addAndLogCapability(caps, originalIndex, str) { if (caps.indexOf(str) === -1) { caps.push(str); Lib.TalkativeLog(`-@-cap : ${originalIndex} : added cap : ${str}`); } else { Lib.TalkativeLog(`-@-cap : ${originalIndex} : ignore duplicated cap : ${str}`); } } /** * get devices IV * * @returns {Gst.Device[]} */ getDevicesIV() { Lib.TalkativeLog('-@-get devices'); var list = this.deviceMonitor.get_devices(); Lib.TalkativeLog(`-@-devices number: ${list.length}`); // Note: // Although the computer may have just one webcam connected to // it, more than one GstDevice may be listed and all pointing to // the same video device (for example /dev/video0. Each // GstDevice is supposed to be used with a specific source, for // example, a pipewiresrc or a v4l2src. For now, we are only // using v4l2src. // See also: Gst.DeviceMonitor.get_providers: pipewiredeviceprovider,decklinkdeviceprovider,v4l2deviceprovider // CLI: "/usr/bin/gst-device-monitor-1.0 Video/Source" // // So, here we filter the devices, that have a device.path property, which // means, these are only v4l2 devices var filtered = list.filter(device => { let props = device.get_properties(); let hasDevice = props != null && props.get_string('device.path') !== null; if (props != null) props.free(); return hasDevice; }); Lib.TalkativeLog(`-@-devices number after filtering for v4l2: ${filtered.length}`); return filtered; } /** * get array name devices IV * * @returns {Array} */ getNameDevices() { Lib.TalkativeLog('-@-get name devices'); let tmpArray = []; for (var index in this._listDevices) { var wcName = this._unspecified_webcam_text; if (this._listDevices[index].display_name !== '') wcName = this._listDevices[index].display_name; tmpArray.push(wcName); } Lib.TalkativeLog(`-@-list devices name: ${tmpArray}`); return tmpArray; } /** * get array caps * * @param {int} index device * @returns {string[]} */ getListCapsDevice(index) { const tmpArray = this._listCaps[index]; Lib.TalkativeLog(`-@-list caps of device: ${tmpArray}`); return tmpArray; } /** * start listening */ startMonitor() { Lib.TalkativeLog('-@-start video devicemonitor'); this.deviceMonitor.start(); } /** * Stop listening */ stopMonitor() { Lib.TalkativeLog('-@-stop video devicemonitor'); this.disconnectSourceBus(); this.deviceMonitor.stop(); } /** * disconect bus */ disconnectSourceBus() { if (this.dmBusId) { this.dmBus.disconnect(this.dmBusId); this.dmBusId = 0; } } });