2020-04-23 20:46:44 +02:00
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
2023-05-25 20:31:22 +02:00
import Gio from 'gi://Gio' ;
import GLib from 'gi://GLib' ;
import Gst from 'gi://Gst?version=1.0' ;
import Gtk from 'gi://Gtk?version=4.0' ;
2020-04-23 20:46:44 +02:00
2023-05-25 20:31:22 +02:00
import { ServiceImplementation } from './dbusService.js' ;
2020-09-14 19:55:17 +02:00
2023-10-01 20:10:33 +02:00
import { ScreencastErrors , ScreencastError } from './misc/dbusErrors.js' ;
2023-08-07 17:13:48 +02:00
import { loadInterfaceXML , loadSubInterfaceXML } from './misc/dbusUtils.js' ;
2023-07-10 02:53:00 -07:00
import * as Signals from './misc/signals.js' ;
2020-04-23 20:46:44 +02:00
const ScreencastIface = loadInterfaceXML ( 'org.gnome.Shell.Screencast' ) ;
const IntrospectIface = loadInterfaceXML ( 'org.gnome.Shell.Introspect' ) ;
const IntrospectProxy = Gio . DBusProxy . makeProxyWrapper ( IntrospectIface ) ;
const ScreenCastIface = loadSubInterfaceXML (
'org.gnome.Mutter.ScreenCast' , 'org.gnome.Mutter.ScreenCast' ) ;
const ScreenCastSessionIface = loadSubInterfaceXML (
'org.gnome.Mutter.ScreenCast.Session' , 'org.gnome.Mutter.ScreenCast' ) ;
const ScreenCastStreamIface = loadSubInterfaceXML (
'org.gnome.Mutter.ScreenCast.Stream' , 'org.gnome.Mutter.ScreenCast' ) ;
const ScreenCastProxy = Gio . DBusProxy . makeProxyWrapper ( ScreenCastIface ) ;
const ScreenCastSessionProxy = Gio . DBusProxy . makeProxyWrapper ( ScreenCastSessionIface ) ;
const ScreenCastStreamProxy = Gio . DBusProxy . makeProxyWrapper ( ScreenCastStreamIface ) ;
const DEFAULT _FRAMERATE = 30 ;
const DEFAULT _DRAW _CURSOR = true ;
2023-10-19 16:09:00 +02:00
const PIPELINE _BLOCKLIST _FILENAME = 'gnome-shell-screencast-pipeline-blocklist' ;
2022-01-29 00:51:31 +01:00
const PIPELINES = [
2023-01-22 19:29:23 +01:00
{
id : 'swenc-dmabuf-h264-openh264' ,
fileExtension : 'mp4' ,
pipelineString :
' capsfilter caps = video / x - raw ( memory : DMABuf ) , max - framerate = % F / 1 ! \
glupload ! glcolorconvert ! gldownload ! \
queue ! \
openh264enc deblocking = off background - detection = false complexity = low adaptive - quantization = false qp - max = 26 qp - min = 26 multi - thread = % T slice - mode = auto ! \
queue ! \
h264parse ! \
mp4mux fragment - duration = 500 fragment - mode = first - moov - then - finalise ' ,
} ,
{
id : 'swenc-memfd-h264-openh264' ,
fileExtension : 'mp4' ,
pipelineString :
' capsfilter caps = video / x - raw , max - framerate = % F / 1 ! \
videoconvert chroma - mode = none dither = none matrix - mode = output - only n - threads = % T ! \
queue ! \
openh264enc deblocking = off background - detection = false complexity = low adaptive - quantization = false qp - max = 26 qp - min = 26 multi - thread = % T slice - mode = auto ! \
queue ! \
h264parse ! \
mp4mux fragment - duration = 500 fragment - mode = first - moov - then - finalise ' ,
} ,
2023-02-11 07:06:14 +01:00
{
2023-10-19 16:09:00 +02:00
id : 'swenc-dmabuf-vp8-vp8enc' ,
2023-01-22 19:43:53 +01:00
fileExtension : 'webm' ,
2023-02-11 07:06:14 +01:00
pipelineString :
' capsfilter caps = video / x - raw ( memory : DMABuf ) , max - framerate = % F / 1 ! \
glupload ! glcolorconvert ! gldownload ! \
queue ! \
vp8enc cpu - used = 16 max - quantizer = 17 deadline = 1 keyframe - mode = disabled threads = % T static - threshold = 1000 buffer - size = 20000 ! \
queue ! \
webmmux ' ,
} ,
2022-01-29 00:51:31 +01:00
{
2023-10-19 16:09:00 +02:00
id : 'swenc-memfd-vp8-vp8enc' ,
2023-01-22 19:43:53 +01:00
fileExtension : 'webm' ,
2022-01-29 00:51:31 +01:00
pipelineString :
' capsfilter caps = video / x - raw , max - framerate = % F / 1 ! \
videoconvert chroma - mode = none dither = none matrix - mode = output - only n - threads = % T ! \
queue ! \
vp8enc cpu - used = 16 max - quantizer = 17 deadline = 1 keyframe - mode = disabled threads = % T static - threshold = 1000 buffer - size = 20000 ! \
queue ! \
webmmux ' ,
} ,
] ;
2020-04-23 20:46:44 +02:00
const PipelineState = {
2022-01-29 00:31:00 +01:00
INIT : 'INIT' ,
2022-01-29 00:51:31 +01:00
STARTING : 'STARTING' ,
2022-01-29 00:31:00 +01:00
PLAYING : 'PLAYING' ,
FLUSHING : 'FLUSHING' ,
STOPPED : 'STOPPED' ,
2022-01-29 00:31:15 +01:00
ERROR : 'ERROR' ,
2020-04-23 20:46:44 +02:00
} ;
const SessionState = {
2022-01-29 00:31:00 +01:00
INIT : 'INIT' ,
ACTIVE : 'ACTIVE' ,
STOPPED : 'STOPPED' ,
2020-04-23 20:46:44 +02:00
} ;
2023-07-10 02:53:00 -07:00
class Recorder extends Signals . EventEmitter {
2023-01-22 19:43:53 +01:00
constructor ( sessionPath , x , y , width , height , filePathStem , options ,
2023-04-26 12:29:57 +02:00
invocation ) {
super ( ) ;
2020-04-23 20:46:44 +02:00
this . _dbusConnection = invocation . get _connection ( ) ;
this . _x = x ;
this . _y = y ;
this . _width = width ;
this . _height = height ;
2023-01-22 19:43:53 +01:00
this . _filePathStem = filePathStem ;
2020-04-23 20:46:44 +02:00
2021-12-17 10:17:35 +03:00
try {
2023-01-22 19:43:53 +01:00
const dir = Gio . File . new _for _path ( filePathStem ) . get _parent ( ) ;
2021-12-17 10:17:35 +03:00
dir . make _directory _with _parents ( null ) ;
} catch ( e ) {
if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . EXISTS ) )
throw e ;
}
2023-05-30 20:23:14 +02:00
this . _pipelineString = null ;
2020-04-23 20:46:44 +02:00
this . _framerate = DEFAULT _FRAMERATE ;
this . _drawCursor = DEFAULT _DRAW _CURSOR ;
2023-10-19 16:09:00 +02:00
this . _blocklistFromPreviousCrashes = [ ] ;
const pipelineBlocklistPath = GLib . build _filenamev (
[ GLib . get _user _runtime _dir ( ) , PIPELINE _BLOCKLIST _FILENAME ] ) ;
this . _pipelineBlocklistFile = Gio . File . new _for _path ( pipelineBlocklistPath ) ;
try {
const [ success _ , contents ] = this . _pipelineBlocklistFile . load _contents ( null ) ;
const decoder = new TextDecoder ( ) ;
this . _blocklistFromPreviousCrashes = JSON . parse ( decoder . decode ( contents ) ) ;
} catch ( e ) {
if ( ! e . matches ( Gio . IOErrorEnum , Gio . IOErrorEnum . NOT _FOUND ) )
throw e ;
}
2020-04-23 20:46:44 +02:00
2022-01-28 23:50:35 +01:00
this . _pipelineState = PipelineState . INIT ;
this . _pipeline = null ;
2020-04-23 20:46:44 +02:00
this . _applyOptions ( options ) ;
this . _watchSender ( invocation . get _sender ( ) ) ;
2022-01-28 23:50:35 +01:00
this . _sessionState = SessionState . INIT ;
2020-04-23 20:46:44 +02:00
this . _initSession ( sessionPath ) ;
}
_applyOptions ( options ) {
for ( const option in options )
2022-08-10 11:56:14 +02:00
options [ option ] = options [ option ] . deepUnpack ( ) ;
2020-04-23 20:46:44 +02:00
if ( options [ 'pipeline' ] !== undefined )
this . _pipelineString = options [ 'pipeline' ] ;
if ( options [ 'framerate' ] !== undefined )
this . _framerate = options [ 'framerate' ] ;
if ( 'draw-cursor' in options )
this . _drawCursor = options [ 'draw-cursor' ] ;
}
2020-09-14 19:55:17 +02:00
_addRecentItem ( ) {
const file = Gio . File . new _for _path ( this . _filePath ) ;
Gtk . RecentManager . get _default ( ) . add _item ( file . get _uri ( ) ) ;
}
2020-04-23 20:46:44 +02:00
_watchSender ( sender ) {
this . _nameWatchId = this . _dbusConnection . watch _name (
sender ,
Gio . BusNameWatcherFlags . NONE ,
null ,
this . _senderVanished . bind ( this ) ) ;
}
_unwatchSender ( ) {
if ( this . _nameWatchId !== 0 ) {
this . _dbusConnection . unwatch _name ( this . _nameWatchId ) ;
this . _nameWatchId = 0 ;
}
}
2022-01-28 23:50:35 +01:00
_teardownPipeline ( ) {
if ( ! this . _pipeline )
return ;
if ( this . _pipeline . set _state ( Gst . State . NULL ) !== Gst . StateChangeReturn . SUCCESS )
log ( 'Failed to set pipeline state to NULL' ) ;
2020-04-23 20:46:44 +02:00
2022-01-28 23:50:35 +01:00
this . _pipelineState = PipelineState . STOPPED ;
this . _pipeline = null ;
2020-04-23 20:46:44 +02:00
}
2022-01-28 23:50:35 +01:00
_stopSession ( ) {
if ( this . _sessionState === SessionState . ACTIVE ) {
this . _sessionState = SessionState . STOPPED ;
this . _sessionProxy . StopSync ( ) ;
}
}
2023-10-01 20:10:33 +02:00
_bailOutOnError ( message , errorDomain = ScreencastErrors , errorCode = ScreencastError . RECORDER _ERROR ) {
const error = new GLib . Error ( errorDomain , errorCode , message ) ;
2023-10-19 16:09:00 +02:00
// If it's a PIPELINE_ERROR, we want to leave the failing pipeline on the
// blocklist for the next time. Other errors are pipeline-independent, so
// reset the blocklist to allow the pipeline to be tried again next time.
if ( ! error . matches ( ScreencastErrors , ScreencastError . PIPELINE _ERROR ) )
this . _updateServiceCrashBlocklist ( [ ... this . _blocklistFromPreviousCrashes ] ) ;
2022-01-28 23:50:35 +01:00
this . _teardownPipeline ( ) ;
2020-04-23 20:46:44 +02:00
this . _unwatchSender ( ) ;
2022-01-28 23:50:35 +01:00
this . _stopSession ( ) ;
if ( this . _startRequest ) {
this . _startRequest . reject ( error ) ;
delete this . _startRequest ;
}
if ( this . _stopRequest ) {
this . _stopRequest . reject ( error ) ;
delete this . _stopRequest ;
}
2023-04-26 12:29:57 +02:00
this . emit ( 'error' , error ) ;
2022-01-28 23:50:35 +01:00
}
2023-10-01 20:10:33 +02:00
_handleFatalPipelineError ( message , errorDomain , errorCode ) {
2022-01-28 23:50:35 +01:00
this . _pipelineState = PipelineState . ERROR ;
2023-10-01 20:10:33 +02:00
this . _bailOutOnError ( message , errorDomain , errorCode ) ;
2022-01-28 23:50:35 +01:00
}
_senderVanished ( ) {
2023-10-01 20:10:33 +02:00
this . _bailOutOnError ( 'Sender has vanished' ) ;
2020-04-23 20:46:44 +02:00
}
_onSessionClosed ( ) {
2022-01-28 23:50:35 +01:00
if ( this . _sessionState === SessionState . STOPPED )
return ; // We closed the session ourselves
this . _sessionState = SessionState . STOPPED ;
2023-10-01 20:10:33 +02:00
this . _bailOutOnError ( 'Session closed unexpectedly' ) ;
2020-04-23 20:46:44 +02:00
}
_initSession ( sessionPath ) {
this . _sessionProxy = new ScreenCastSessionProxy ( Gio . DBus . session ,
'org.gnome.Mutter.ScreenCast' ,
sessionPath ) ;
this . _sessionProxy . connectSignal ( 'Closed' , this . _onSessionClosed . bind ( this ) ) ;
}
2023-10-19 16:09:00 +02:00
_updateServiceCrashBlocklist ( blocklist ) {
try {
if ( blocklist . length === 0 ) {
this . _pipelineBlocklistFile . delete ( null ) ;
} else {
this . _pipelineBlocklistFile . replace _contents (
JSON . stringify ( blocklist ) , null , false ,
Gio . FileCreateFlags . NONE , null ) ;
}
} catch ( e ) {
console . log ( ` Failed to update pipeline-blocklist file: ${ e . message } ` ) ;
}
}
2022-01-29 00:51:31 +01:00
_tryNextPipeline ( ) {
2023-01-22 19:43:53 +01:00
if ( this . _filePath ) {
GLib . unlink ( this . _filePath ) ;
delete this . _filePath ;
}
2022-01-29 00:51:31 +01:00
const { done , value : pipelineConfig } = this . _pipelineConfigs . next ( ) ;
if ( done ) {
2023-10-01 20:10:33 +02:00
this . _handleFatalPipelineError ( 'All pipelines failed to start' ,
ScreencastErrors , ScreencastError . ALL _PIPELINES _FAILED ) ;
2022-01-29 00:51:31 +01:00
return ;
}
2023-10-19 16:09:00 +02:00
if ( this . _blocklistFromPreviousCrashes . includes ( pipelineConfig . id ) ) {
console . info ( ` Skipping pipeline ' ${ pipelineConfig . id } ' due to pipeline blocklist ` ) ;
this . _tryNextPipeline ( ) ;
return ;
}
2023-04-26 19:13:27 +02:00
if ( this . _pipeline ) {
if ( this . _pipeline . set _state ( Gst . State . NULL ) !== Gst . StateChangeReturn . SUCCESS )
log ( 'Failed to set pipeline state to NULL' ) ;
this . _pipeline = null ;
}
2022-01-29 00:51:31 +01:00
try {
this . _pipeline = this . _createPipeline ( this . _nodeId , pipelineConfig ,
this . _framerate ) ;
2023-10-19 16:09:00 +02:00
// Add the current pipeline to the blocklist, so it is skipped next
// time in case we crash; we'll remove it again on success or on
// non-pipeline-related failures.
this . _updateServiceCrashBlocklist (
[ ... this . _blocklistFromPreviousCrashes , pipelineConfig . id ] ) ;
} catch ( error ) {
2022-01-29 00:51:31 +01:00
this . _tryNextPipeline ( ) ;
return ;
}
2020-04-23 20:46:44 +02:00
const bus = this . _pipeline . get _bus ( ) ;
bus . add _watch ( bus , this . _onBusMessage . bind ( this ) ) ;
2022-02-03 11:26:59 +01:00
const retval = this . _pipeline . set _state ( Gst . State . PLAYING ) ;
2020-04-23 20:46:44 +02:00
2022-02-03 11:26:59 +01:00
if ( retval === Gst . StateChangeReturn . SUCCESS ||
retval === Gst . StateChangeReturn . ASYNC ) {
// We'll wait for the state change message to PLAYING on the bus
} else {
2022-01-29 00:51:31 +01:00
this . _tryNextPipeline ( ) ;
2022-02-03 11:26:59 +01:00
}
2020-04-23 20:46:44 +02:00
}
2023-05-30 20:23:14 +02:00
* _getPipelineConfigs ( ) {
if ( this . _pipelineString ) {
yield {
pipelineString :
` capsfilter caps=video/x-raw,max-framerate=%F/1 ! ${ this . _pipelineString } ` ,
} ;
return ;
}
const fallbackSupported =
Gst . Registry . get ( ) . check _feature _version ( 'pipewiresrc' , 0 , 3 , 67 ) ;
if ( fallbackSupported )
yield * PIPELINES ;
else
yield PIPELINES . at ( - 1 ) ;
}
2022-02-03 11:10:36 +01:00
startRecording ( ) {
return new Promise ( ( resolve , reject ) => {
this . _startRequest = { resolve , reject } ;
const [ streamPath ] = this . _sessionProxy . RecordAreaSync (
this . _x , this . _y ,
this . _width , this . _height ,
{
'is-recording' : GLib . Variant . new ( 'b' , true ) ,
'cursor-mode' : GLib . Variant . new ( 'u' , this . _drawCursor ? 1 : 0 ) ,
} ) ;
this . _streamProxy = new ScreenCastStreamProxy ( Gio . DBus . session ,
'org.gnome.ScreenCast.Stream' ,
streamPath ) ;
this . _streamProxy . connectSignal ( 'PipeWireStreamAdded' ,
( _proxy , _sender , params ) => {
const [ nodeId ] = params ;
2022-01-29 00:51:31 +01:00
this . _nodeId = nodeId ;
this . _pipelineState = PipelineState . STARTING ;
2023-05-30 20:23:14 +02:00
this . _pipelineConfigs = this . _getPipelineConfigs ( ) ;
2022-01-29 00:51:31 +01:00
this . _tryNextPipeline ( ) ;
2022-02-03 11:10:36 +01:00
} ) ;
this . _sessionProxy . StartSync ( ) ;
this . _sessionState = SessionState . ACTIVE ;
} ) ;
2020-04-23 20:46:44 +02:00
}
2022-02-03 11:10:36 +01:00
stopRecording ( ) {
if ( this . _startRequest )
return Promise . reject ( new Error ( 'Unable to stop recorder while still starting' ) ) ;
return new Promise ( ( resolve , reject ) => {
this . _stopRequest = { resolve , reject } ;
this . _pipelineState = PipelineState . FLUSHING ;
this . _pipeline . send _event ( Gst . Event . new _eos ( ) ) ;
} ) ;
2020-04-23 20:46:44 +02:00
}
_onBusMessage ( bus , message , _ ) {
switch ( message . type ) {
2022-02-03 11:26:59 +01:00
case Gst . MessageType . STATE _CHANGED : {
const [ , newState ] = message . parse _state _changed ( ) ;
2022-01-29 00:51:31 +01:00
if ( this . _pipelineState === PipelineState . STARTING &&
2022-02-03 11:26:59 +01:00
message . src === this . _pipeline &&
newState === Gst . State . PLAYING ) {
this . _pipelineState = PipelineState . PLAYING ;
2023-01-22 19:43:53 +01:00
this . _startRequest . resolve ( this . _filePath ) ;
2022-02-03 11:26:59 +01:00
delete this . _startRequest ;
}
break ;
}
2020-04-23 20:46:44 +02:00
case Gst . MessageType . EOS :
switch ( this . _pipelineState ) {
2022-01-29 00:51:31 +01:00
case PipelineState . INIT :
2022-01-29 00:40:24 +01:00
case PipelineState . STOPPED :
case PipelineState . ERROR :
// In these cases there should be no pipeline, so should never happen
break ;
2022-01-29 00:51:31 +01:00
case PipelineState . STARTING :
// This is something we can handle, try to switch to the next pipeline
this . _tryNextPipeline ( ) ;
break ;
2022-01-29 00:40:24 +01:00
case PipelineState . PLAYING :
this . _addRecentItem ( ) ;
2023-10-01 20:10:33 +02:00
this . _handleFatalPipelineError ( 'Unexpected EOS message' ,
ScreencastErrors , ScreencastError . PIPELINE _ERROR ) ;
2022-01-29 00:40:24 +01:00
break ;
2020-04-23 20:46:44 +02:00
case PipelineState . FLUSHING :
2023-10-19 16:09:00 +02:00
// The pipeline ran successfully and we didn't crash; we can remove it
// from the blocklist again now.
this . _updateServiceCrashBlocklist ( [ ... this . _blocklistFromPreviousCrashes ] ) ;
2022-01-28 23:50:35 +01:00
this . _addRecentItem ( ) ;
2020-04-23 20:46:44 +02:00
2022-01-29 00:40:24 +01:00
this . _teardownPipeline ( ) ;
2022-01-28 23:50:35 +01:00
this . _unwatchSender ( ) ;
2020-04-23 20:46:44 +02:00
this . _stopSession ( ) ;
2022-01-28 23:50:35 +01:00
this . _stopRequest . resolve ( ) ;
delete this . _stopRequest ;
2020-04-23 20:46:44 +02:00
break ;
default :
break ;
}
break ;
2022-01-29 00:40:24 +01:00
case Gst . MessageType . ERROR :
switch ( this . _pipelineState ) {
2022-01-29 00:51:31 +01:00
case PipelineState . INIT :
2022-01-29 00:40:24 +01:00
case PipelineState . STOPPED :
case PipelineState . ERROR :
// In these cases there should be no pipeline, so should never happen
break ;
2022-01-29 00:51:31 +01:00
case PipelineState . STARTING :
// This is something we can handle, try to switch to the next pipeline
this . _tryNextPipeline ( ) ;
break ;
2022-01-29 00:40:24 +01:00
case PipelineState . PLAYING :
2023-10-01 20:10:33 +02:00
case PipelineState . FLUSHING : {
const [ error ] = message . parse _error ( ) ;
2023-10-02 14:00:45 +02:00
if ( error . matches ( Gst . ResourceError , Gst . ResourceError . NO _SPACE _LEFT ) ) {
this . _handleFatalPipelineError ( 'Out of disk space' ,
ScreencastErrors , ScreencastError . OUT _OF _DISK _SPACE ) ;
} else {
this . _handleFatalPipelineError (
` GStreamer error while in state ${ this . _pipelineState } : ${ error . message } ` ,
ScreencastErrors , ScreencastError . PIPELINE _ERROR ) ;
}
2023-10-01 20:10:33 +02:00
2022-01-29 00:40:24 +01:00
break ;
2023-10-01 20:10:33 +02:00
}
2022-01-29 00:40:24 +01:00
default :
break ;
}
break ;
2020-04-23 20:46:44 +02:00
default :
break ;
}
return true ;
}
2022-01-29 00:51:31 +01:00
_substituteVariables ( pipelineDescr , framerate ) {
2020-04-23 20:46:44 +02:00
const numProcessors = GLib . get _num _processors ( ) ;
const numThreads = Math . min ( Math . max ( 1 , numProcessors ) , 64 ) ;
2022-01-29 00:51:31 +01:00
return pipelineDescr . replaceAll ( '%T' , numThreads ) . replaceAll ( '%F' , framerate ) ;
2020-04-23 20:46:44 +02:00
}
2022-01-29 00:51:31 +01:00
_createPipeline ( nodeId , pipelineConfig , framerate ) {
2023-01-22 19:43:53 +01:00
const { fileExtension , pipelineString } = pipelineConfig ;
2022-01-29 00:51:31 +01:00
const finalPipelineString = this . _substituteVariables ( pipelineString , framerate ) ;
2023-01-22 19:43:53 +01:00
this . _filePath = ` ${ this . _filePathStem } . ${ fileExtension } ` ;
2020-04-23 20:46:44 +02:00
2022-01-29 00:51:31 +01:00
const fullPipeline = `
2020-04-23 20:46:44 +02:00
pipewiresrc path = $ { nodeId }
do - timestamp = true
keepalive - time = 1000
resend - last = true !
2022-01-29 00:51:31 +01:00
$ { finalPipelineString } !
2020-08-11 01:34:03 +02:00
filesink location = "${this._filePath}" ` ;
2020-04-23 20:46:44 +02:00
2022-01-29 00:51:31 +01:00
return Gst . parse _launch _full ( fullPipeline , null ,
Gst . ParseFlags . FATAL _ERRORS ) ;
2020-04-23 20:46:44 +02:00
}
2023-07-10 02:53:00 -07:00
}
2020-04-23 20:46:44 +02:00
2023-05-25 20:31:22 +02:00
export const ScreencastService = class extends ServiceImplementation {
2022-10-19 12:43:29 +02:00
static canScreencast ( ) {
2022-01-29 00:51:31 +01:00
if ( ! Gst . init _check ( null ) )
return false ;
let elements = [
2022-10-24 14:01:03 +02:00
'pipewiresrc' ,
'filesink' ,
] ;
2022-01-29 00:51:31 +01:00
if ( elements . some ( e => Gst . ElementFactory . find ( e ) === null ) )
return false ;
// The fallback pipeline must be available, the other ones are not
// guaranteed to work because they depend on hw encoders.
const fallbackPipeline = PIPELINES . at ( - 1 ) ;
elements = fallbackPipeline . pipelineString . split ( '!' ) . map (
e => e . trim ( ) . split ( ' ' ) . at ( 0 ) ) ;
if ( elements . every ( e => Gst . ElementFactory . find ( e ) !== null ) )
return true ;
return false ;
2022-10-19 12:43:29 +02:00
}
2020-04-23 20:46:44 +02:00
constructor ( ) {
super ( ScreencastIface , '/org/gnome/Shell/Screencast' ) ;
2022-11-06 11:56:41 +01:00
this . hold ( ) ; // gstreamer initializing can take a bit
2022-10-19 12:43:29 +02:00
this . _canScreencast = ScreencastService . canScreencast ( ) ;
2020-04-23 20:46:44 +02:00
Gst . init ( null ) ;
2021-01-20 02:48:24 +01:00
Gtk . init ( ) ;
2020-04-23 20:46:44 +02:00
2022-11-06 11:56:41 +01:00
this . release ( ) ;
2020-04-23 20:46:44 +02:00
this . _recorders = new Map ( ) ;
this . _senders = new Map ( ) ;
this . _lockdownSettings = new Gio . Settings ( {
schema _id : 'org.gnome.desktop.lockdown' ,
} ) ;
this . _proxy = new ScreenCastProxy ( Gio . DBus . session ,
'org.gnome.Mutter.ScreenCast' ,
'/org/gnome/Mutter/ScreenCast' ) ;
this . _introspectProxy = new IntrospectProxy ( Gio . DBus . session ,
'org.gnome.Shell.Introspect' ,
'/org/gnome/Shell/Introspect' ) ;
}
2022-10-19 12:43:29 +02:00
get ScreencastSupported ( ) {
return this . _canScreencast ;
}
2020-04-23 20:46:44 +02:00
_removeRecorder ( sender ) {
2022-01-28 23:50:35 +01:00
if ( ! this . _recorders . delete ( sender ) )
return ;
2020-04-23 20:46:44 +02:00
if ( this . _recorders . size === 0 )
this . release ( ) ;
}
_addRecorder ( sender , recorder ) {
this . _recorders . set ( sender , recorder ) ;
if ( this . _recorders . size === 1 )
this . hold ( ) ;
}
_getAbsolutePath ( filename ) {
if ( GLib . path _is _absolute ( filename ) )
return filename ;
2023-01-04 14:03:14 +01:00
const videoDir =
GLib . get _user _special _dir ( GLib . UserDirectory . DIRECTORY _VIDEOS ) ||
GLib . get _home _dir ( ) ;
2020-04-23 20:46:44 +02:00
return GLib . build _filenamev ( [ videoDir , filename ] ) ;
}
_generateFilePath ( template ) {
let filename = '' ;
let escape = false ;
2023-01-22 19:43:53 +01:00
// FIXME: temporarily detect and strip .webm prefix to avoid breaking
// external consumers of our API, remove this again
if ( template . endsWith ( '.webm' ) ) {
console . log ( "'file_template' for screencast includes '.webm' file-extension. Passing the file-extension as part of the filename has been deprecated, pass the 'file_template' without a file-extension instead." ) ;
template = template . substring ( 0 , template . length - '.webm' . length ) ;
}
2020-04-23 20:46:44 +02:00
[ ... template ] . forEach ( c => {
if ( escape ) {
switch ( c ) {
case '%' :
filename += '%' ;
break ;
case 'd' : {
const datetime = GLib . DateTime . new _now _local ( ) ;
2022-05-20 17:40:35 +01:00
const datestr = datetime . format ( '%Y-%m-%d' ) ;
2020-04-23 20:46:44 +02:00
2022-05-20 17:40:35 +01:00
filename += datestr ;
2020-04-23 20:46:44 +02:00
break ;
}
case 't' : {
const datetime = GLib . DateTime . new _now _local ( ) ;
2022-05-20 17:40:35 +01:00
const datestr = datetime . format ( '%H-%M-%S' ) ;
2020-04-23 20:46:44 +02:00
2022-05-20 17:40:35 +01:00
filename += datestr ;
2020-04-23 20:46:44 +02:00
break ;
}
default :
log ( ` Warning: Unknown escape ${ c } ` ) ;
}
escape = false ;
} else if ( c === '%' ) {
escape = true ;
} else {
filename += c ;
}
} ) ;
if ( escape )
filename += '%' ;
return this . _getAbsolutePath ( filename ) ;
}
2022-02-03 11:10:36 +01:00
async ScreencastAsync ( params , invocation ) {
2020-04-23 20:46:44 +02:00
if ( this . _lockdownSettings . get _boolean ( 'disable-save-to-disk' ) ) {
2023-10-01 20:10:33 +02:00
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . SAVE _TO _DISK _DISABLED ,
'Saving to disk is disabled' ) ;
2020-04-23 20:46:44 +02:00
return ;
}
const sender = invocation . get _sender ( ) ;
if ( this . _recorders . get ( sender ) ) {
2023-10-01 20:10:33 +02:00
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . ALREADY _RECORDING ,
'Service is already recording' ) ;
2020-04-23 20:46:44 +02:00
return ;
}
const [ sessionPath ] = this . _proxy . CreateSessionSync ( { } ) ;
const [ fileTemplate , options ] = params ;
const [ screenWidth , screenHeight ] = this . _introspectProxy . ScreenSize ;
2023-01-22 19:43:53 +01:00
const filePathStem = this . _generateFilePath ( fileTemplate ) ;
2020-04-23 20:46:44 +02:00
let recorder ;
try {
recorder = new Recorder (
sessionPath ,
0 , 0 ,
screenWidth , screenHeight ,
2023-01-22 19:43:53 +01:00
filePathStem ,
2020-04-23 20:46:44 +02:00
options ,
2023-04-26 12:29:57 +02:00
invocation ) ;
2020-04-23 20:46:44 +02:00
} catch ( error ) {
log ( ` Failed to create recorder: ${ error . message } ` ) ;
2023-10-01 20:10:33 +02:00
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . RECORDER _ERROR ,
error . message ) ;
2020-04-23 20:46:44 +02:00
return ;
}
this . _addRecorder ( sender , recorder ) ;
try {
2023-01-22 19:43:53 +01:00
const pathWithExtension = await recorder . startRecording ( ) ;
invocation . return _value ( GLib . Variant . new ( '(bs)' , [ true , pathWithExtension ] ) ) ;
2020-04-23 20:46:44 +02:00
} catch ( error ) {
log ( ` Failed to start recorder: ${ error . message } ` ) ;
this . _removeRecorder ( sender ) ;
2023-10-01 20:10:33 +02:00
if ( error instanceof GLib . Error ) {
invocation . return _gerror ( error ) ;
} else {
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . RECORDER _ERROR ,
error . message ) ;
}
return ;
2020-04-23 20:46:44 +02:00
}
2023-04-26 12:29:57 +02:00
recorder . connect ( 'error' , ( r , error ) => {
2023-10-01 20:10:33 +02:00
log ( ` Fatal error while recording: ${ error . message } ` ) ;
2023-04-26 12:29:57 +02:00
this . _removeRecorder ( sender ) ;
this . _dbusImpl . emit _signal ( 'Error' ,
2023-10-01 20:10:33 +02:00
new GLib . Variant ( '(ss)' , [
Gio . DBusError . encode _gerror ( error ) ,
error . message ,
] ) ) ;
2023-04-26 12:29:57 +02:00
} ) ;
2020-04-23 20:46:44 +02:00
}
2022-02-03 11:10:36 +01:00
async ScreencastAreaAsync ( params , invocation ) {
2020-04-23 20:46:44 +02:00
if ( this . _lockdownSettings . get _boolean ( 'disable-save-to-disk' ) ) {
2023-10-01 20:10:33 +02:00
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . SAVE _TO _DISK _DISABLED ,
'Saving to disk is disabled' ) ;
2020-04-23 20:46:44 +02:00
return ;
}
const sender = invocation . get _sender ( ) ;
if ( this . _recorders . get ( sender ) ) {
2023-10-01 20:10:33 +02:00
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . ALREADY _RECORDING ,
'Service is already recording' ) ;
2020-04-23 20:46:44 +02:00
return ;
}
const [ sessionPath ] = this . _proxy . CreateSessionSync ( { } ) ;
const [ x , y , width , height , fileTemplate , options ] = params ;
2023-01-22 19:43:53 +01:00
const filePathStem = this . _generateFilePath ( fileTemplate ) ;
2020-04-23 20:46:44 +02:00
let recorder ;
try {
recorder = new Recorder (
sessionPath ,
x , y ,
width , height ,
2023-01-22 19:43:53 +01:00
filePathStem ,
2020-04-23 20:46:44 +02:00
options ,
2023-04-26 12:29:57 +02:00
invocation ) ;
2020-04-23 20:46:44 +02:00
} catch ( error ) {
log ( ` Failed to create recorder: ${ error . message } ` ) ;
2023-10-01 20:10:33 +02:00
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . RECORDER _ERROR ,
error . message ) ;
2020-04-23 20:46:44 +02:00
return ;
}
this . _addRecorder ( sender , recorder ) ;
try {
2023-01-22 19:43:53 +01:00
const pathWithExtension = await recorder . startRecording ( ) ;
invocation . return _value ( GLib . Variant . new ( '(bs)' , [ true , pathWithExtension ] ) ) ;
2020-04-23 20:46:44 +02:00
} catch ( error ) {
log ( ` Failed to start recorder: ${ error . message } ` ) ;
this . _removeRecorder ( sender ) ;
2023-10-01 20:10:33 +02:00
if ( error instanceof GLib . Error ) {
invocation . return _gerror ( error ) ;
} else {
invocation . return _error _literal ( ScreencastErrors ,
ScreencastError . RECORDER _ERROR ,
error . message ) ;
}
return ;
2020-04-23 20:46:44 +02:00
}
2023-04-26 12:29:57 +02:00
recorder . connect ( 'error' , ( r , error ) => {
2023-10-01 20:10:33 +02:00
log ( ` Fatal error while recording: ${ error . message } ` ) ;
2023-04-26 12:29:57 +02:00
this . _removeRecorder ( sender ) ;
this . _dbusImpl . emit _signal ( 'Error' ,
2023-10-01 20:10:33 +02:00
new GLib . Variant ( '(ss)' , [
Gio . DBusError . encode _gerror ( error ) ,
error . message ,
] ) ) ;
2023-04-26 12:29:57 +02:00
} ) ;
2020-04-23 20:46:44 +02:00
}
2022-02-03 11:10:36 +01:00
async StopScreencastAsync ( params , invocation ) {
2020-04-23 20:46:44 +02:00
const sender = invocation . get _sender ( ) ;
const recorder = this . _recorders . get ( sender ) ;
if ( ! recorder ) {
invocation . return _value ( GLib . Variant . new ( '(b)' , [ false ] ) ) ;
return ;
}
2022-02-03 11:10:36 +01:00
try {
await recorder . stopRecording ( ) ;
} catch ( error ) {
log ( ` ${ sender } : Error while stopping recorder: ${ error . message } ` ) ;
} finally {
2020-04-23 20:46:44 +02:00
this . _removeRecorder ( sender ) ;
invocation . return _value ( GLib . Variant . new ( '(b)' , [ true ] ) ) ;
2022-02-03 11:10:36 +01:00
}
2020-04-23 20:46:44 +02:00
}
} ;