Cannot assign to read only property 'role' when calling transport.consume

Calling transport.consume is resulting in a readonly error for me:

                  const consumer = transport
                    .consume({
                      id,
                      producerId,
                      kind,
                      rtpParameters,
                    })
                    .then((res) => {
                      console.log("!!res", res)
                    })
                    .catch((e) => {
                      console.log("!!e", e)
                    })

The params that I’m passing in come from:

const { transport, params } = await createWebRtcTransport(router)

the error:

TypeError: Cannot assign to read only property 'role' of object '#<Object>'
    at RemoteSdp.updateDtlsRole (RemoteSdp.js:88:35)
    at Chrome111.setupTransport (Chrome111.js:672:25)
    at Chrome111.receive (Chrome111.js:528:24)
    at async transport.createPendingConsumers() [as task] (webpack://web-matchapp/node_modules/mediasoup-client/lib/Transport.js?:564:33)
    at async AwaitQueue.execute (index.js:172:28)

I can try to provide more info if anyone could suggest which information might be useful? or I could post my entire back and forth between client and server if that level of information would be required to troubleshoot this. Sorry if my question is vague, I’m just in the process of learning to use the library.

Please read the API documentation properly. The above code doesn’t make sense. The returned value of transport.consume() is not a Consumer but a Promise that resolves to a Consumer.

Sorry, do you mean that the variable name I’ve chosen doesn’t make sense because it won’t actually be a consumer that’s returned but rather a promise of a consumer. The error I’m receiving is occurring within the transport.consume call, rather than with anything I’m doing with my perhaps incorrectly named variable. Not sure if I’m just misunderstanding what you’re saying though.

Yes, that’s a separate naming problem. In your case you are probably using Vue or MobX or something like that and passing their special proxy objects to the mediasoup-client API instead of passing pure JS objects.

Ah okay, will try to improve my naming conventions, thanks. I don’t think I’m using any kind of proxy objects or anything like that. I really just have this one monolithic frontend function and two backend functions and it works almost all of the way through (i.e. I don’t get any issue when I check canConsume, only after, when I attempt to consume via the client consumerTransport):

Client function:

  const handleViewStream = () => {
    viewStream({ variables: { params: { username: streamer.username } } }).then(
      async ({ data }) => {
        if (data && "viewStream" in data) {
          const {
            viewStream: { params, routerCapabilities },
          } = data

          try {
            const consumeDevice = new mediasoup.Device()

            await consumeDevice.load({
              routerRtpCapabilities: routerCapabilities,
            })

            const consumerTransport = consumeDevice.createRecvTransport(params)

            consumerTransport.on(
              "connect",
              async ({ dtlsParameters }, callback, errback) => {
                console.log("!!!!!!!!!!!!!!!!!!!connect")

                // socket?.emit("connectConsumerTransport", {
                //   transportId: transport.id,
                //   dtlsParameters,
                //   streamerUsername: streamer.username,
                // })
                // socket.on("consumerConnected", async function (data) {
                //   console.log("!![ms]consumerConnected", data)
                //   callback()
                // })
              }
            )

            consumerTransport.on("connectionstatechange", (state) => {
              console.log("!![ms]connectionStateChange", data)
              switch (state) {
                case "connecting":
                  console.log("!!Consumer: ICE State Connecting")
                  break
                case "connected":
                  console.log("!!Consumer: ICE State Connected")
                  // socket?.emit("resume", { streamerUsername: streamer.username })
                  break
                case "failed":
                  console.log("!!Consumer: ICE State Failed")
                  consumerTransport.close()
                  // socket?.emit("stopViewingStream", {
                  //   streamerUsername: streamer.username,
                  //   viewerUsername: user.username,
                  // })
                  break
                default:
                  break
              }
            })

            consume({
              variables: {
                rtpCapabilities: consumeDevice.rtpCapabilities,
                streamerUsername: streamer.username,
              },
            }).then(async ({ data }) => {
              if (data && "consume" in data) {
                const { subData } = data.consume
                const { id, producerId, kind, rtpParameters } = subData

                consumerTransport
                  .consume({
                    id,
                    producerId,
                    kind,
                    rtpParameters,
                  })
                  .then((res) => {
                    console.log("!!res", res)
                  })
                  .catch((e) => {
                    console.log("!!e", e)
                  })
              }
            })
          } catch (error) {
            if (error.name === "UnsupportedError") {
              console.log("!!Browser not supported")
            }
          }
        }
      }
    )
  }

Server side functions:

export const getStream = async (db: Kysely<Database>, { args, context }) => {
  try {
    const { user: currentUser } = context
    const {
      params: { username },
    } = args

    if (!currentUser) {
      return new Error("Not logged in")
    }

    if (!streams[username]) {
      return new Error("User is not live")
    }

    const routerCapabilities = await getRouterCapabilities()

    const { transport, params } = await createWebRtcTransport(router)

    streams[username] = {
      ...streams[username],
      consumerTransports: {
        ...streams[username].consumerTransports,
        [currentUser.username]: transport,
      },
    }

    return { params, routerCapabilities }
  } catch (error) {
    console.log("!!error", error)
    return new Error("Request could not be processed")
  }
}

export const consume = async (db: Kysely<Database>, { args, context }) => {
  try {
    const { user: currentUser } = context
    const { rtpCapabilities, streamerUsername } = args

    if (
      !router.canConsume({
        producerId: streams[streamerUsername].producer.id,
        rtpCapabilities,
      })
    ) {
      console.log("!!Cannot Consume")
      return
    }

    try {
      const newConsumer = await streams[streamerUsername].consumerTransports[
        currentUser.username
      ].consume({
        producerId: streams[streamerUsername].producer.id,
        rtpCapabilities,
        paused: streams[streamerUsername].producer.kind === "video",
      })

      streams[streamerUsername] = {
        ...streams[streamerUsername],
        consumers: {
          ...streams[streamerUsername].consumers,
          [currentUser.username]: newConsumer,
        },
      }

      const subData = {
        producerId: streams[streamerUsername].producer.id,
        id: newConsumer.id,
        kind: newConsumer.kind,
        rtpParameters: newConsumer.rtpParameters,
        type: newConsumer.type,
        producerPaused: newConsumer.producerPaused,
      }

      return {
        subData: subData,
        stream: streams[streamerUsername],
      }
    } catch (error) {
      console.log("!!error", error)
      return
    }
  } catch (error) {
    console.log("!!error", error)
  }
}

I guess my use of in-memory data structures is pretty horrible but I’m kind of aware of that. I’m just trying to learn at the moment so will improve things after. I think the producer stuff is all working now and I think almost all of the consumer stuff is working, it just seems to run into this issue of the read only property right at the end when I call consumerTransport.consume.

I appreciate you probably don’t have the time or desire to look through the above code. Just wanted to post it for the sake of a more complete thread and in case anyone else fancies taking a look.

This is the mediasoup-client code involved into your issue:

	private async setupTransport({
		localDtlsRole,
		localSdpObject,
	}: {
		localDtlsRole: DtlsRole;
		localSdpObject?: any;
	}): Promise<void> {
		if (!localSdpObject) {
			localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
		}

		// Get our local DTLS parameters.
		const dtlsParameters = sdpCommonUtils.extractDtlsParameters({
			sdpObject: localSdpObject,
		});

		// Set our DTLS role.
		dtlsParameters.role = localDtlsRole;

		// Update the remote DTLS role in the SDP.
		this._remoteSdp!.updateDtlsRole(
			localDtlsRole === 'client' ? 'server' : 'client'
		);

Here it parses a SDP (a string) here:

localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);

Then it extracts DTLS parameters from the parsed SDP (which is just a JS object) calling:

		const dtlsParameters = sdpCommonUtils.extractDtlsParameters({
			sdpObject: localSdpObject,
		});

which is as follows:

export function extractDtlsParameters({
	sdpObject,
}: {
	sdpObject: any;
}): DtlsParameters {
	let setup = sdpObject.setup;
	let fingerprint = sdpObject.fingerprint;

	if (!setup || !fingerprint) {
		const mediaObject = (sdpObject.media || []).find(
			(m: { port: number }) => m.port !== 0
		);

		if (mediaObject) {
			setup ??= mediaObject.setup;
			fingerprint ??= mediaObject.fingerprint;
		}
	}

	if (!setup) {
		throw new Error('no a=setup found at SDP session or media level');
	} else if (!fingerprint) {
		throw new Error('no a=fingerprint found at SDP session or media level');
	}

	let role: DtlsRole | undefined;

	switch (setup) {
		case 'active': {
			role = 'client';

			break;
		}

		case 'passive': {
			role = 'server';

			break;
		}

		case 'actpass': {
			role = 'auto';

			break;
		}
	}

	const dtlsParameters: DtlsParameters = {
		role,
		fingerprints: [
			{
				algorithm: fingerprint.type,
				value: fingerprint.hash,
			},
		],
	};

	return dtlsParameters;
}

And then it changes the role value of the returned dtlsParameters object:

dtlsParameters.role = localDtlsRole;

Nothing fancy here. So I hace no idea about why this happens to you but nothing in mediasoup-client internals should produce it.

updateDtlsRole modifies another dtlsParameters object - the one that is passed to Device.createRecvTransport (nested in its argument), then to the Transport constructor and the handler.
Here it is hidden in the data.viewStream.params:

There could be anything, depending on how this object was created.

1 Like

params just looks like this:

    params: {
        id: string;
        iceParameters: IceParameters;
        iceCandidates: IceCandidate[];
        dtlsParameters: DtlsParameters;
        sctpParameters: SctpParameters;
    };

it comes from here:

const { transport, params } = await createWebRtcTransport(router)

return { params, routerCapabilities }

which is a call to:

import { Router } from "mediasoup/node/lib/types"
import { config } from "./config"

export const createWebRtcTransport = async (mediasoupRouter: Router) => {
  const { maxIncomeBitrate, initialAvailableOutgoingBitrate } =
    config.mediasoup.webRtcTransport

  const transport = await mediasoupRouter.createWebRtcTransport({
    listenIps: config.mediasoup.webRtcTransport.listenIps,
    enableUdp: true,
    enableTcp: true,
    preferUdp: true,
    initialAvailableOutgoingBitrate,
  })

  if (maxIncomeBitrate) {
    try {
      await transport.setMaxIncomingBitrate(maxIncomeBitrate)
    } catch (error) {
      console.log("!!error", error)
    }
  }

  return {
    transport,
    params: {
      id: transport.id,
      iceParameters: transport.iceParameters,
      iceCandidates: transport.iceCandidates,
      dtlsParameters: transport.dtlsParameters,
      sctpParameters: transport.sctpParameters,
    },
  }
}

I guess it sounds like ibc is correct, if this params object is supposed to be modified by updateDtlsRole as you say snnz and I’m getting a read-only error on it, then for some reason, it’s unable to modify the params object and that’s what I need to look into. Thanks for the help in troubleshooting this. Will feedback if I make progress in case it can help someone else.

Yeah, you guys had it right. Tested with this and got a connect result:

let params = {
  ...data.viewStream.params,
  dtlsParameters: {
    role: data.viewStream.params.dtlsParameters.role,
    fingerprints: data.viewStream.params.dtlsParameters.fingerprints,
    },
  }

Thanks for the help, I appreciate it.