Audio and video artifacts when using Gstreamer playbin pipeline in GJS

I’ve created a custom class that wraps a GStreamer pipeline in GJS (GNOME Shell extension)
using playbin with video output into appsink and audio into autoaudiosink:

const videoBin   = new Gst.Bin({ name: 'video-bin' });
const videoConvert  = Gst.ElementFactory.make('videoconvert', 'videoconvert');
const videoSink  = Gst.ElementFactory.make('appsink', 'video-sink');

if (!videoConvert || !videoSink) {
    throw new Error('Failed to create video elements');
}

videoSink.set_property('caps', Gst.Caps.from_string('video/x-raw,format=RGBA'));
videoSink.set_property('max-buffers', 1);
videoSink.set_property('drop', true);
videoSink.set_property('sync', true);
  
videoBin.add(videoConvert);
videoBin.add(videoSink);
videoConvert.link(videoSink);

const videoGhostPad = Gst.GhostPad.new('sink', videoConvert.get_static_pad('sink'));
videoBin.add_pad(videoGhostPad);

let pipeline = Gst.ElementFactory.make('playbin', 'pipeline');
if (!pipeline) {
    throw new Error('Failed to create playbin element')
}
pipeline.set_property('uri', GLib.filename_to_uri(this._videoPath, null));
pipeline.set_property('video-sink', videoBin);

if (this._volume > 0) {
    const audioBin      = new Gst.Bin({ name: 'audio-bin' });
    const audioQueue    = Gst.ElementFactory.make('queue2', 'audio-queue');
    const audioConvert  = Gst.ElementFactory.make('audioconvert',  'audioconvert');
    const audioResample = Gst.ElementFactory.make('audioresample',  'audioresample');
    const audioSink = Gst.ElementFactory.make('autoaudiosink', 'audio-sink');
    
    if (!audioConvert || !audioResample || !audioSink) {
        throw new Error('Failed to create audio elements');
    }

    audioQueue.set_property('max-size-buffers', 0); // unlimited
    audioQueue.set_property('max-size-time', 2 * Gst.SECOND); // 2 second buffer
    audioQueue.set_property('max-size-bytes', 0); // unlimited

    audioSink.set_property('sync', true);
    
    audioBin.add(audioConvert);
    audioBin.add(audioResample);
    audioBin.add(audioQueue);
    audioBin.add(audioSink);

    audioConvert.link(audioResample);
    audioResample.link(audioQueue);
    audioQueue.link(audioSink);

    const audioGhostPad = Gst.GhostPad.new('sink', audioConvert.get_static_pad('sink'));
    audioBin.add_pad(audioGhostPad);
    pipeline.set_property('audio-sink', audioBin);
    pipeline.set_property('volume', this._volume);
} else {
    const fakeSink = Gst.ElementFactory.make('fakesink', 'audio-fake');
    pipeline.set_property('audio-sink', fakeSink);
}

this._pipeline = pipeline;
this._bus = pipeline.get_bus();
this._videoSink = videoSink;

this._initBusWatch();

const interval = 1000 / this._framerate;
this._timeoutId = this._startFetchTimer(interval)
if (!this._timeoutId) {
    throw new Error('Failed to create the fetch timer')
}

this._initialized = true;
return true;

It works well except for two issues:

1. Green frame at video start
Occasionally the first pulled sample from appsink contains a fully green frame before
actual video data appears. I currently skip the first frame as a workaround but this
feels hacky and unreliable. Is there a proper way to ensure the first valid frame is
ready before pulling samples?

2. Click/pop sound on state change and volume change
When transitioning between PLAYING and PAUSED states, or when changing the volume
property on playbin, there is a short clicking sound. I’ve tried:

  • autoaudiosink
  • pulsesink
  • pipewiresink
  • Adding queue2 with larger buffers before the sink
  • Short volume fade before state change

None of these fully resolve the issue. Is this a known problem with playbin state
transitions in GJS? Is there a recommended way to handle clean pause/resume without
audio artifacts?

GStreamer version: 1.26.10