onShakaMessage_(event) {
    const message = shaka.cast.CastUtils.deserialize(event.data);
    shaka.log.debug('CastReceiver: message', message);

    switch (message['type']) {
      case 'init':
        // Reset update message frequency values after initialization.
        this.updateNumber_ = 0;
        this.startUpdatingUpdateNumber_ = false;
        this.initialStatusUpdatePending_ = true;

        this.initState_(message['initState'], message['appData']);
        // The sender is supposed to reflect the cast system volume after
        // connecting.  Using fakeVolumeChangeEvent_() would create a race on
        // the sender side, since it would have volume properties, but no
        // others.
        // This would lead to hasRemoteProperties() being true, even though a
        // complete set had never been sent.
        // Now that we have init state, this is a good time for the first update
        // message anyway.
        this.pollAttributes_();
        break;
      case 'appData':
        this.appDataCallback_(message['appData']);
        break;
      case 'set': {
        const targetName = message['targetName'];
        const property = message['property'];
        const value = message['value'];

        if (targetName == 'video') {
          // Volume attributes must be rerouted to the system.
          const manager = cast.receiver.CastReceiverManager.getInstance();
          if (property == 'volume') {
            manager.setSystemVolumeLevel(value);
            break;
          } else if (property == 'muted') {
            manager.setSystemVolumeMuted(value);
            break;
          }
        }

        this.targets_[targetName][property] = value;
        break;
      }
      case 'call': {
        const targetName = message['targetName'];
        const methodName = message['methodName'];
        const args = message['args'];
        const target = this.targets_[targetName];
        // eslint-disable-next-line prefer-spread
        target[methodName].apply(target, args);
        break;
      }
      case 'asyncCall': {
        const targetName = message['targetName'];
        const methodName = message['methodName'];
        if (targetName == 'player' && methodName == 'load') {
          // Reset update message frequency values after a load.
          this.updateNumber_ = 0;
          this.startUpdatingUpdateNumber_ = false;
        }
        const args = message['args'];
        const id = message['id'];
        const senderId = event.senderId;
        const target = this.targets_[targetName];
        // eslint-disable-next-line prefer-spread
        let p = target[methodName].apply(target, args);
        if (targetName == 'player' && methodName == 'load') {
          // Wait until the manifest has actually loaded to send another media
          // info message, so on a new load it doesn't send the old info over.
          p = p.then(() => {
            this.initialStatusUpdatePending_ = true;
          });
        }
        // Replies must go back to the specific sender who initiated, so that we
        // don't have to deal with conflicting IDs between senders.
        p.then(
            (res) => this.sendAsyncComplete_(
                senderId, id, /* error= */ null, res),
            (error) => this.sendAsyncComplete_(
                senderId, id, error, /* response= */ null));
        break;
      }
    }
  }
Since this method is in the compiled library, make sure all messages are
read with quoted properties.
@param {!cast.receiver.CastMessageBus.Event} event
@private