GStreamer WHIP streams randomly showing on Android

Hi all,

I’m using GStreamer to publish 2 camera streams to a room.
When I join the room on a desktop I see both streams working perfectly without issues.
On my Android device it also works when I join the room whether I use the Chrome browser or when I build it into an APK with Capacitor.

But on budget devices like;

  • Honor X9 tablet
  • Samsung A52 phone

We retrieve the streams but it only shows one.
When I refresh I randomly only see camera1 or camera2. While my other devices are watching simultaneously they always show both streams.

I’m using the following GStreamer code to stream both videos:

"v4l2src device=/dev/video0 ! video/x-raw,width=960,height=720,framerate=30/1 ! videoconvert ! queue ! x264enc bitrate=1000 byte-stream=false key-int-max=60 threads=0 tune=zerolatency speed-preset=ultrafast ! rtph264pay config-interval=-1 ! queue ! application/x-rtp,media=video,encoding-name=H264"

"v4l2src device=/dev/video1 ! video/x-raw,width=960,height=720,framerate=30/1 ! videoconvert ! queue ! x264enc bitrate=1000 byte-stream=false key-int-max=60 threads=0 tune=zerolatency speed-preset=ultrafast ! rtph264pay config-interval=-1 ! queue ! application/x-rtp,media=video,encoding-name=H264"

We are using the janus-gateway NPM package to spectate a room:

        this.janus.attach({
            plugin: 'janus.plugin.videoroom',
            opaqueId: this.opaqueId,
            success: (pluginHandle: JanusJS.PluginHandle) => {
                Janus.log('Attached for subscriber', pluginHandle.getId(), roomId);

                feedPluginHandle = pluginHandle;

                if (type === 'spectator' && userMedia) {
                    if (this.subscribersRoomHandlesMap.has(roomId)) {
                        this.subscribersRoomHandlesMap.get(roomId).push(feedPluginHandle);
                    } else {
                        this.subscribersRoomHandlesMap.set(roomId, [feedPluginHandle]);
                    }
                    this.subscribersHandleMap.set(feedId, feedPluginHandle);
                } else if (type === 'external' && this.ownUserMedia) {
                    this.ownSubscribersRoomHandles.set(feedId, feedPluginHandle);
                }

                Janus.log(
                    'Plugin attached! (' + feedPluginHandle.getPlugin() + ', id=' + feedPluginHandle.getId() + ')'
                );
                Janus.log('  -- This is a subscriber');

                feedPluginHandle.send({
                    message: subscribe,
                    success: () => {
                        Janus.log('Subscriber handle success', feedId);

                        feedPluginHandle.send({
                            message: {
                                request: 'subscribe',
                                streams: [{ feed: feedId }],
                            },
                            success: () => {
                                Janus.log('Subscribed to feed:', feedId);
                            },
                            error: (error: any) => {
                                console.error('Error joining room as subscriber:', error);
                            },
                        });
                    },
                    error: (error: any) => {
                        console.error('Error joining room as subscriber:', error);
                    },
                });
            },
            error: (error: string) => {
                console.error(`Error attaching to the room ${roomId}`, error);
            },
            iceState: (state) => {
                Janus.log('ICE state (feed #' + feedId + ') changed to ' + state);
            },
            webrtcState: (on) => {
                Janus.log(
                    'Janus says this WebRTC PeerConnection (feed #' + feedId + ') is ' + (on ? 'up' : 'down') + ' now'
                );
            },
            slowLink: (uplink, lost, mid) => {
                Janus.warn(
                    'Janus reports problems ' +
                        (uplink ? 'sending' : 'receiving') +
                        ' packets on mid ' +
                        mid +
                        ' (' +
                        lost +
                        ' lost packets)'
                );
            },
            onmessage: (msg, jsep: JanusJS.JSEP) => {
                Janus.log('ONMESSAGE-s', msg, jsep);

                if (jsep) {
                    Janus.log('HANDLING JSEP');
                    this.handleSubscriberJsep(feedPluginHandle, jsep, roomId);
                }
            },
            onremotetrack: (track: DCStreamTrack, mid, on, event: { reason: string }) => {
                Janus.log(
                    'Handle 2: Remote track handler: ' + feedPluginHandle.getId(),
                    feedPluginHandle.detached,
                    on,
                    userMedia
                );
                Janus.log('Handle 2: Received remote track', track.kind, track, 'from feed', feedId, mid, on, event);
                if (!feedPluginHandle.detached) {
                    if (on) {
                        track.feedId = feedId;
                        if (type === 'spectator' && userMedia) {
                            Janus.log('Binding users userMedia', displayMetaData.facingMode, mid, track.id);
                            if (track.kind === 'video') {
                                let forceTrack = true;
                                if (
                                    isSpectating &&
                                    displayMetaData.facingMode === FacingModes.PLAYER &&
                                    user?.room?.disablePlayerCamForSpectators === true
                                ) {
                                    forceTrack = false;
                                }

                                if (forceTrack) {
                                    userMedia.addVideoTrack(track, mid, displayMetaData);

                                    if (record) {
                                        try {
                                            userMedia.startMediaRecorder(displayMetaData.facingMode);
                                        } catch (err) {
                                            console.error(err);
                                        }
                                    }

                                    if (userMedia.remoteEvents$) {
                                        userMedia.remoteEvents$.next(mid);
                                    }
                                }
                            } else if (track.kind === 'audio') {
                                userMedia.addAudioTrack(track, displayMetaData);
                            }
                        } else if (type === 'external' && this.ownUserMedia) {
                            Janus.log('Binding own userMedia', displayMetaData.facingMode, mid, track.id);
                            if (track.kind === 'video' && !this.isPublishingVideo) {
                                this.ownUserMedia.addVideoTrack(track, mid, displayMetaData);

                                if (this.ownUserMedia.remoteEvents$) {
                                    this.ownUserMedia.remoteEvents$.next(mid);
                                }
                            } else if (track.kind === 'audio' && !this.isPublishingAudio) {
                                this.ownUserMedia.addAudioTrack(track, displayMetaData);
                            }

                            if (this._janusEventService && this.janus?.isConnected()) {
                                this._janusEventService.emitEvent({
                                    type: JanusEventType.OwnExternalDeviceConnected,
                                    user: user,
                                });
                            }
                        }
                    } else {
                        if (type === 'spectator' && userMedia) {
                            userMedia.removeTrack(track, displayMetaData);
                        } else if (type === 'external' && this.ownUserMedia) {
                            this.ownUserMedia.removeTrack(track, displayMetaData);
                        }
                    }
                }

                if (track.kind === 'video') {
                    if (type === 'spectator' && userMedia) {
                        if (
                            displayMetaData.facingMode === FacingModes.BOARD &&
                            userMedia.videoStreams?.board?.feedId === feedId
                        ) {
                            if (displayMetaData.scale) {
                                userMedia.videoStreams.board.scale = displayMetaData.scale;
                            }
                        }

                        let activeStreams = null;
                        if (activeStreams !== false) {
                            activeStreams = userMedia.videoStreams.board?.isActive;
                        }
                        if (activeStreams !== false) {
                            activeStreams = userMedia.videoStreams.player?.isActive;
                        }
                        userMedia.videoStreams.activeStreams = activeStreams;
                    } else if (type === 'external' && this.ownUserMedia) {
                        if (
                            displayMetaData.facingMode === FacingModes.BOARD &&
                            this.ownUserMedia.videoStreams?.board?.feedId === feedId
                        ) {
                            if (displayMetaData.scale) {
                                this.ownUserMedia.videoStreams.board.scale = displayMetaData.scale;
                            }
                        }

                        let activeStreams = null;
                        if (activeStreams !== false) {
                            activeStreams = this.ownUserMedia.videoStreams.board?.isActive;
                        }
                        if (activeStreams !== false) {
                            activeStreams = this.ownUserMedia.videoStreams.player?.isActive;
                        }
                        this.ownUserMedia.videoStreams.activeStreams = activeStreams;
                    }

                    if (on) {
                        if (type === 'spectator' && userMedia) {
                            userMedia.videoStreams.kind = displayMetaData.kind;
                        } else if (type === 'external' && this.ownUserMedia) {
                            this.ownUserMedia.videoStreams.kind = displayMetaData.kind;
                        }
                    }
                }
            },
            oncleanup: () => {
                Janus.log(' ::: Got a cleanup notification (remote feed ' + feedId + ') :::');
            },
        });

The addVideoTrack() fuction adds a track to the existing MediaStream.
Again, the streams are working fine on iPhone’s and newer Android phones, so it seems like there is an issue somewhere in our janus-gateaway code.

Maybe the budget devices use hardware decoders for H.264, and can only decode one at a time (just guessing). Try using a codec that will need a software decoder, like VP8, to see if that works instead.

When we join two different rooms, each containing one video stream, it is working fine. So it can handle more streams… But when I join one room, containing 2 camera streams, these are the three outcomes:

  • It shows cam1, not cam2
  • It shows cam2, not cam 1
  • It shows no cams

These three are random. Whenever I rejoin the room, it can have a different outcome. Can I change something to our janus-gateway code to accept JSEPs or something?

This is the final step of our product, but we are stuck on this…