Trình chuyển đổi đầu ra

Output Switcher là một tính năng của Cast SDK cho phép chuyển đổi liền mạch giữa chế độ phát nội dung cục bộ và từ xa, bắt đầu từ Android 13. Mục tiêu là giúp các ứng dụng của người gửi dễ dàng và nhanh chóng kiểm soát vị trí phát nội dung. Output Switcher sử dụng thư viện MediaRouter để chuyển đổi chế độ phát nội dung giữa loa điện thoại, thiết bị Bluetooth đã ghép nối và các thiết bị hỗ trợ Cast từ xa. Có thể chia các trường hợp sử dụng thành những trường hợp sau:

Hãy tải xuống và sử dụng mẫu bên dưới để tham khảo cách triển khai Trình chuyển đổi đầu ra trong ứng dụng Âm thanh. Hãy xem README.md đi kèm để biết hướng dẫn về cách chạy mẫu.

Tải mẫu xuống

Bạn nên bật Trình chuyển đổi đầu ra để hỗ trợ từ xa cục bộ đến từ xa và từ xa đến cục bộ bằng các bước nêu trong hướng dẫn này. Bạn không cần thực hiện thêm bước nào để chuyển giữa loa thiết bị cục bộ và thiết bị Bluetooth đã ghép nối.

Ứng dụng âm thanh là các ứng dụng hỗ trợ Google Cast cho Âm thanh trong phần cài đặt Ứng dụng thu trong Google Cast SDK Developer Console

Giao diện người dùng của Trình chuyển đổi đầu ra

Output Switcher hiển thị các thiết bị cục bộ và từ xa có sẵn, cũng như trạng thái hiện tại của thiết bị, bao gồm cả trường hợp thiết bị được chọn, đang kết nối, mức âm lượng hiện tại. Nếu có các thiết bị khác ngoài thiết bị hiện tại, thì bạn có thể nhấp vào thiết bị khác để chuyển nội dung nghe nhìn phát cho thiết bị đã chọn.

Vấn đề đã biết

  • Các phiên phát nội dung đa phương tiện được tạo để phát trên thiết bị sẽ bị đóng và tạo lại khi chuyển sang thông báo về SDK truyền.

Điểm truy cập

Thông báo về nội dung nghe nhìn

Nếu một ứng dụng đăng thông báo về nội dung nghe nhìn bằng MediaSession để phát trên thiết bị (phát cục bộ), thì ở góc trên cùng bên phải của thông báo về nội dung nghe nhìn sẽ hiển thị khối thông báo có tên thiết bị (chẳng hạn như loa điện thoại) mà nội dung hiện đang được phát trên đó. Khi nhấn vào khối thông báo, giao diện người dùng hệ thống của hộp thoại Output Switcher sẽ mở ra.

Cài đặt âm lượng

Bạn cũng có thể kích hoạt giao diện người dùng hệ thống của hộp thoại Output Switcher bằng cách nhấp vào các nút âm lượng vật lý trên thiết bị, nhấn vào biểu tượng cài đặt ở dưới cùng rồi nhấn vào văn bản "Play <App Name> trên <Cast Device>".

Tóm tắt các bước

Điều kiện tiên quyết

  1. Di chuyển ứng dụng Android hiện có sang AndroidX.
  2. Cập nhật build.gradle của ứng dụng để sử dụng phiên bản tối thiểu bắt buộc của SDK Trình gửi Android cho Trình chuyển đổi đầu ra:
    dependencies {
      ...
      implementation 'com.google.android.gms:play-services-cast-framework:21.2.0'
      ...
    }
  3. Ứng dụng hỗ trợ thông báo về nội dung nghe nhìn.
  4. Thiết bị chạy Android 13.

Thiết lập thông báo về nội dung nghe nhìn

Để sử dụng Trình chuyển đổi đầu ra, ứng dụng âm thanhvideo phải tạo thông báo về nội dung nghe nhìn nhằm cho thấy trạng thái phát và các nút điều khiển nội dung nghe nhìn để phát trên thiết bị. Việc này yêu cầu bạn phải tạo một MediaSession, đặt MediaStyle bằng mã thông báo của MediaSession và đặt các chế độ điều khiển nội dung nghe nhìn trên thông báo.

Nếu bạn hiện không sử dụng MediaStyleMediaSession, thì đoạn mã dưới đây sẽ cho biết cách thiết lập hai thuộc tính này và bạn có thể xem hướng dẫn để thiết lập lệnh gọi lại phiên phát nội dung đa phương tiện cho ứng dụng âm thanhvideo:

Kotlin
// Create a media session. NotificationCompat.MediaStyle
// PlayerService is your own Service or Activity responsible for media playback.
val mediaSession = MediaSessionCompat(this, "PlayerService")

// Create a MediaStyle object and supply your media session token to it.
val mediaStyle = Notification.MediaStyle().setMediaSession(mediaSession.sessionToken)

// Create a Notification which is styled by your MediaStyle object.
// This connects your media session to the media controls.
// Don't forget to include a small icon.
val notification = Notification.Builder(this@PlayerService, CHANNEL_ID)
    .setStyle(mediaStyle)
    .setSmallIcon(R.drawable.ic_app_logo)
    .build()

// Specify any actions which your users can perform, such as pausing and skipping to the next track.
val pauseAction: Notification.Action = Notification.Action.Builder(
        pauseIcon, "Pause", pauseIntent
    ).build()
notification.addAction(pauseAction)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    // Create a media session. NotificationCompat.MediaStyle
    // PlayerService is your own Service or Activity responsible for media playback.
    MediaSession mediaSession = new MediaSession(this, "PlayerService");

    // Create a MediaStyle object and supply your media session token to it.
    Notification.MediaStyle mediaStyle = new Notification.MediaStyle().setMediaSession(mediaSession.getSessionToken());

    // Specify any actions which your users can perform, such as pausing and skipping to the next track.
    Notification.Action pauseAction = Notification.Action.Builder(pauseIcon, "Pause", pauseIntent).build();

    // Create a Notification which is styled by your MediaStyle object.
    // This connects your media session to the media controls.
    // Don't forget to include a small icon.
    String CHANNEL_ID = "CHANNEL_ID";
    Notification notification = new Notification.Builder(this, CHANNEL_ID)
        .setStyle(mediaStyle)
        .setSmallIcon(R.drawable.ic_app_logo)
        .addAction(pauseAction)
        .build();
}

Ngoài ra, để điền thông tin về nội dung nghe nhìn vào thông báo, bạn cần thêm siêu dữ liệu và trạng thái phát của nội dung nghe nhìn vào MediaSession.

Để thêm siêu dữ liệu vào MediaSession, hãy sử dụng setMetaData() và cung cấp tất cả hằng số MediaMetadata có liên quan cho nội dung nghe nhìn của bạn trong MediaMetadataCompat.Builder().

Kotlin
mediaSession.setMetadata(MediaMetadataCompat.Builder()
    // Title
    .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

    // Artist
    // Could also be the channel name or TV series.
    .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

    // Album art
    // Could also be a screenshot or hero image for video content
    // The URI scheme needs to be "content", "file", or "android.resource".
    .putString(
        MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)
    )

    // Duration
    // If duration isn't set, such as for live broadcasts, then the progress
    // indicator won't be shown on the seekbar.
    .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

    .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setMetadata(
        new MediaMetadataCompat.Builder()
        // Title
        .putString(MediaMetadata.METADATA_KEY_TITLE, currentTrack.title)

        // Artist
        // Could also be the channel name or TV series.
        .putString(MediaMetadata.METADATA_KEY_ARTIST, currentTrack.artist)

        // Album art
        // Could also be a screenshot or hero image for video content
        // The URI scheme needs to be "content", "file", or "android.resource".
        .putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, currentTrack.albumArtUri)

        // Duration
        // If duration isn't set, such as for live broadcasts, then the progress
        // indicator won't be shown on the seekbar.
        .putLong(MediaMetadata.METADATA_KEY_DURATION, currentTrack.duration)

        .build()
    );
}

Để thêm trạng thái phát vào MediaSession, hãy sử dụng setPlaybackState() và cung cấp mọi hằng số PlaybackStateCompat có liên quan cho nội dung nghe nhìn trong PlaybackStateCompat.Builder().

Kotlin
mediaSession.setPlaybackState(
    PlaybackStateCompat.Builder()
        .setState(
            PlaybackStateCompat.STATE_PLAYING,

            // Playback position
            // Used to update the elapsed time and the progress bar.
            mediaPlayer.currentPosition.toLong(),

            // Playback speed
            // Determines the rate at which the elapsed time changes.
            playbackSpeed
        )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
)
Java
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
    mediaSession.setPlaybackState(
        new PlaybackStateCompat.Builder()
            .setState(
                 PlaybackStateCompat.STATE_PLAYING,

                // Playback position
                // Used to update the elapsed time and the progress bar.
                mediaPlayer.currentPosition.toLong(),

                // Playback speed
                // Determines the rate at which the elapsed time changes.
                playbackSpeed
            )

        // isSeekable
        // Adding the SEEK_TO action indicates that seeking is supported
        // and makes the seekbar position marker draggable. If this is not
        // supplied seek will be disabled but progress will still be shown.
        .setActions(PlaybackStateCompat.ACTION_SEEK_TO)
        .build()
    );
}

Hành vi thông báo của ứng dụng video

Ứng dụng video hoặc ứng dụng âm thanh không hỗ trợ tính năng phát cục bộ ở chế độ nền nên có hành vi cụ thể đối với thông báo về nội dung nghe nhìn để tránh vấn đề khi gửi lệnh đa phương tiện trong trường hợp không hỗ trợ phát nội dung:

  • Đăng thông báo về nội dung nghe nhìn khi phát nội dung nghe nhìn cục bộ và ứng dụng đang chạy ở nền trước.
  • Tạm dừng phát trên thiết bị và đóng thông báo khi ứng dụng đang chạy trong nền.
  • Khi ứng dụng quay lại nền trước, quá trình phát cục bộ sẽ tiếp tục và thông báo sẽ được đăng lại.

Bật Trình chuyển đổi đầu ra trong AndroidManifest.xml

Để bật Trình chuyển đổi đầu ra, bạn cần thêm MediaTransferReceiver vào AndroidManifest.xml của ứng dụng. Nếu không, tính năng này sẽ không được bật và cờ tính năng từ xa đến cục bộ cũng sẽ không hợp lệ.

<application>
    ...
    <receiver
         android:name="androidx.mediarouter.media.MediaTransferReceiver"
         android:exported="true">
    </receiver>
    ...
</application>

MediaTransferReceiver là một broadcast receiver cho phép chuyển nội dung nghe nhìn giữa các thiết bị có giao diện người dùng hệ thống. Hãy xem tài liệu tham khảo về MediaTransferReceiver để biết thêm thông tin.

Từ cục bộ đến từ xa

Khi người dùng chuyển chế độ phát từ thiết bị cục bộ sang điều khiển từ xa, SDK Truyền sẽ tự động bắt đầu phiên Truyền. Tuy nhiên, các ứng dụng cần xử lý quá trình chuyển đổi từ thiết bị cục bộ sang điều khiển từ xa, ví dụ: dừng quá trình phát trên thiết bị rồi tải nội dung nghe nhìn trên thiết bị Truyền. Ứng dụng nên nghe lệnh Truyền SessionManagerListener bằng các lệnh gọi lại onSessionStarted()onSessionEnded(), đồng thời xử lý thao tác khi nhận được lệnh gọi lại Cast SessionManager. Các ứng dụng phải đảm bảo rằng các lệnh gọi lại này vẫn hoạt động khi hộp thoại Output Switcher (Trình chuyển đổi đầu ra) được mở và ứng dụng không chạy ở nền trước.

Cập nhật SessionManagerListener để truyền trong nền

Phiên bản Cast cũ đã hỗ trợ tính năng từ xa cục bộ đến từ xa khi ứng dụng chạy trên nền trước. Trải nghiệm Truyền thông thường bắt đầu khi người dùng nhấp vào biểu tượng Truyền trong ứng dụng và chọn một thiết bị để truyền trực tuyến nội dung nghe nhìn. Trong trường hợp này, ứng dụng cần đăng ký vào SessionManagerListener, trong onCreate() hoặc onStart() và huỷ đăng ký trình nghe trong onStop() hoặc onDestroy() của hoạt động của ứng dụng.

Với trải nghiệm mới là truyền bằng Trình chuyển đổi đầu ra, các ứng dụng có thể bắt đầu truyền khi đang ở chế độ nền. Điều này đặc biệt hữu ích đối với các ứng dụng âm thanh đăng thông báo khi phát trong nền. Các ứng dụng có thể đăng ký trình nghe SessionManager trong onCreate() của dịch vụ và huỷ đăng ký trong onDestroy() của dịch vụ. Bằng cách này, ứng dụng phải luôn nhận được các lệnh gọi lại cục bộ đến từ xa (chẳng hạn như onSessionStarted) khi ứng dụng đang chạy ở chế độ nền.

Nếu ứng dụng sử dụng MediaBrowserService, bạn nên đăng ký SessionManagerListener tại đó.

Kotlin
class MyService : Service() {
    private var castContext: CastContext? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext
            .getSessionManager()
            .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext
                .getSessionManager()
                .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
        }
    }
}
Java
public class MyService extends Service {
  private CastContext castContext;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
    }
  }
}

Với bản cập nhật này, tính năng từ xa cục bộ đến từ xa hoạt động giống như tính năng truyền truyền thống khi ứng dụng chạy trong nền và bạn không cần phải làm gì thêm để chuyển từ thiết bị Bluetooth sang thiết bị Truyền.

Từ xa đến cục bộ

Nút chuyển đầu ra cho phép chuyển từ chế độ phát từ xa sang loa điện thoại hoặc thiết bị Bluetooth cục bộ. Bạn có thể bật tính năng này bằng cách gắn cờ setRemoteToLocalEnabled thành true trên CastOptions.

Đối với trường hợp thiết bị gửi hiện tại tham gia một phiên hiện có với nhiều người gửi và ứng dụng cần kiểm tra xem nội dung nghe nhìn hiện tại có được phép chuyển cục bộ hay không, ứng dụng nên sử dụng lệnh gọi lại onTransferred của SessionTransferCallback để kiểm tra SessionState.

Đặt cờ setRemoteToLocalEnabled

CastOptions cung cấp setRemoteToLocalEnabled để hiện hoặc ẩn loa điện thoại và các thiết bị Bluetooth cục bộ dưới dạng các mục tiêu chuyển sang trong hộp thoại Trình chuyển đổi đầu ra khi có một phiên Truyền đang hoạt động.

Kotlin
class CastOptionsProvider : OptionsProvider {
    fun getCastOptions(context: Context?): CastOptions {
        ...
        return Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
    }
}
Java
public class CastOptionsProvider implements OptionsProvider {
    @Override
    public CastOptions getCastOptions(Context context) {
        ...
        return new CastOptions.Builder()
            ...
            .setRemoteToLocalEnabled(true)
            .build()
  }
}

Tiếp tục phát trên thiết bị

Các ứng dụng hỗ trợ chuyển dữ liệu từ xa sang cục bộ phải đăng ký SessionTransferCallback để nhận được thông báo khi sự kiện xảy ra, từ đó kiểm tra xem nội dung nghe nhìn có được phép chuyển và tiếp tục phát trên máy hay không.

CastContext#addSessionTransferCallback(SessionTransferCallback) cho phép một ứng dụng đăng ký SessionTransferCallback đồng thời theo dõi các lệnh gọi lại onTransferredonTransferFailed khi một người gửi được chuyển sang chế độ phát cục bộ.

Sau khi huỷ đăng ký SessionTransferCallback, ứng dụng sẽ không còn nhận được SessionTransferCallback.

SessionTransferCallback là phần mở rộng của lệnh gọi lại SessionManagerListener hiện có và được kích hoạt sau khi onSessionEnded được kích hoạt. Do đó, thứ tự của các lệnh gọi lại từ xa đến cục bộ là:

  1. onTransferring
  2. onSessionEnding
  3. onSessionEnded
  4. onTransferred

Vì khối thông báo về nội dung nghe nhìn có thể mở Trình chuyển đổi đầu ra khi ứng dụng chạy ở chế độ nền và truyền, nên các ứng dụng cần xử lý quá trình chuyển sang cục bộ theo cách khác nhau tuỳ thuộc vào việc ứng dụng có hỗ trợ tính năng phát ở chế độ nền hay không. Trong trường hợp quá trình chuyển không thành công, onTransferFailed sẽ kích hoạt bất cứ khi nào lỗi xảy ra.

Các ứng dụng hỗ trợ tính năng phát trong nền

Đối với các ứng dụng hỗ trợ tính năng phát trong nền (thường là ứng dụng âm thanh), bạn nên dùng Service (ví dụ: MediaBrowserService). Các dịch vụ phải nghe lệnh gọi lại onTransferred và tiếp tục phát trên thiết bị cả khi ứng dụng đang chạy ở nền trước hoặc nền.

Kotlin
class MyService : Service() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyService extends Service {
    private CastContext castContext;
    private SessionTransferCallback sessionTransferCallback;

    @Override
    protected void onCreate() {
        castContext = CastContext.getSharedInstance(this);
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession.class);
        sessionTransferCallback = new MySessionTransferCallback();
        castContext.addSessionTransferCallback(sessionTransferCallback);
    }

    @Override
    protected void onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession.class);
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback);
            }
        }
    }

    public static class MySessionTransferCallback extends SessionTransferCallback {
        public MySessionTransferCallback() {}

        @Override
        public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
            // Perform necessary steps prior to onTransferred
        }

        @Override
        public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                                  SessionState sessionState) {
            if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.
                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        @Override
        public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                     @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
            // Handle transfer failure.
        }
    }
}

Ứng dụng không hỗ trợ tính năng phát trong nền

Đối với các ứng dụng không hỗ trợ tính năng phát trong nền (thường là ứng dụng video), bạn nên nghe lệnh gọi lại onTransferred và tiếp tục phát trên thiết bị nếu ứng dụng đang chạy trên nền trước.

Nếu chạy ở chế độ nền thì ứng dụng sẽ tạm dừng phát và lưu trữ thông tin cần thiết từ SessionState (ví dụ: siêu dữ liệu của nội dung nghe nhìn và vị trí phát). Khi ứng dụng chạy ở nền trước từ chế độ nền, quá trình phát cục bộ sẽ tiếp tục với thông tin đã lưu trữ.

Kotlin
class MyActivity : AppCompatActivity() {
    private var castContext: CastContext? = null
    private var sessionTransferCallback: SessionTransferCallback? = null
    protected fun onCreate() {
        castContext = CastContext.getSharedInstance(this)
        castContext.getSessionManager()
                   .addSessionManagerListener(sessionManagerListener, CastSession::class.java)
        sessionTransferCallback = MySessionTransferCallback()
        castContext.addSessionTransferCallback(sessionTransferCallback)
    }

    protected fun onDestroy() {
        if (castContext != null) {
            castContext.getSessionManager()
                       .removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
            if (sessionTransferCallback != null) {
                castContext.removeSessionTransferCallback(sessionTransferCallback)
            }
        }
    }

    class MySessionTransferCallback : SessionTransferCallback() {
        fun onTransferring(@SessionTransferCallback.TransferType transferType: Int) {
            // Perform necessary steps prior to onTransferred
        }

        fun onTransferred(@SessionTransferCallback.TransferType transferType: Int,
                          sessionState: SessionState?) {
            if (transferType == SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
                // Remote stream is transferred to the local device.

                // Retrieve information from the SessionState to continue playback on the local player.
            }
        }

        fun onTransferFailed(@SessionTransferCallback.TransferType transferType: Int,
                             @SessionTransferCallback.TransferFailedReason transferFailedReason: Int) {
            // Handle transfer failure.
        }
    }
}
Java
public class MyActivity extends AppCompatActivity {
  private CastContext castContext;
  private SessionTransferCallback sessionTransferCallback;

  @Override
  protected void onCreate() {
     castContext = CastContext.getSharedInstance(this);
     castContext
        .getSessionManager()
        .addSessionManagerListener(sessionManagerListener, CastSession.class);
     sessionTransferCallback = new MySessionTransferCallback();
     castContext.addSessionTransferCallback(sessionTransferCallback);
  }

  @Override
  protected void onDestroy() {
    if (castContext != null) {
       castContext
          .getSessionManager()
          .removeSessionManagerListener(sessionManagerListener, CastSession.class);
      if (sessionTransferCallback != null) {
         castContext.removeSessionTransferCallback(sessionTransferCallback);
      }
    }
  }

  public static class MySessionTransferCallback extends SessionTransferCallback {
    public MySessionTransferCallback() {}

    @Override
    public void onTransferring(@SessionTransferCallback.TransferType int transferType) {
        // Perform necessary steps prior to onTransferred
    }

    @Override
    public void onTransferred(@SessionTransferCallback.TransferType int transferType,
                               SessionState sessionState) {
      if (transferType==SessionTransferCallback.TRANSFER_TYPE_FROM_REMOTE_TO_LOCAL) {
        // Remote stream is transferred to the local device.

        // Retrieve information from the SessionState to continue playback on the local player.
      }
    }

    @Override
    public void onTransferFailed(@SessionTransferCallback.TransferType int transferType,
                                 @SessionTransferCallback.TransferFailedReason int transferFailedReason) {
      // Handle transfer failure.
    }
  }
}