耶耶耶耶耶 发表于 2024-9-11 12:37:08

【重生之我在学Android原生】Media3

干系文章

【重生之我在学Android原生】ContentProvider(Java)
【重生之我在学Android原生】Media3
前言

内容颇多,尽量从简
ExoPlayer使用

官方文档
参考文章
实现效果

Android(java)
使用ExoPlayer播放视频,自定义ExoPlayer界面,记载播放位置(横屏竖屏切换/切换至后台等)
案例实现

创建项目

https://i-blog.csdnimg.cn/blog_migrate/55a829304a3a4828492b7d0503d6573c.png
https://i-blog.csdnimg.cn/blog_migrate/45e532e40e54e3d2e3a2e0b5b96c2f71.png
添加依赖

https://i-blog.csdnimg.cn/blog_migrate/12455b7e2afbdf14a94c31f9323d6508.png
Sync 一下
/// Jetpack Media3 ExoPlayer
    implementation ("androidx.media3:media3-exoplayer:1.3.1")
    implementation ("androidx.media3:media3-ui:1.3.1")
    implementation ("androidx.media3:media3-common:1.3.1")
转到activity_main.xml

https://i-blog.csdnimg.cn/blog_migrate/22a24a65a36548f93545ae725d9d53d4.png
选择Player.View
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.media3.ui.PlayerView
      android:id="@+id/video_view"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
player初始化

要在Activity的生命周期中完成player的初始化烧毁等
https://i-blog.csdnimg.cn/blog_migrate/876d1eb84feb0a85657cfa104864c047.png
https://i-blog.csdnimg.cn/blog_migrate/54ec3da2ea61faf8ec3f1b15e7640b10.png
package com.test.exoplayerexampleapplication;

import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.ui.PlayerView;

import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
    }

    @OptIn(markerClass = UnstableApi.class) @Override
    protected void onStart() {
      super.onStart();
      if (Util.SDK_INT >= 24) {
            initializePlayer();
      }
    }

    @OptIn(markerClass = UnstableApi.class) @Override
    protected void onResume() {
      super.onResume();
      if (Util.SDK_INT < 24) {
            initializePlayer();
      }
    }

    private void initializePlayer() {}
}
定义PlayerView,ExoPlayer

    private PlayerView playerView;
    private ExoPlayer exoPlayer;
    private final Uri videoOneUri = Uri.parse("http://www.w3school.com.cn/example/html5/mov_bbb.mp4");
在AndroidManifest.xml中定义网络权限,假如链接是Http,还需加userCleartextTraffic
https://i-blog.csdnimg.cn/blog_migrate/2f3d1d18e4efcd33c1c3cdf2978bc715.png
<uses-permission android:name="android.permission.INTERNET" />
android:usesCleartextTraffic="true"
定义initializePlayer()方法
private void initializePlayer() {
      playerView = findViewById(R.id.video_view);
      exoPlayer = new ExoPlayer.Builder(this).build();
      playerView.setPlayer(exoPlayer);
      MediaItem mediaItem = MediaItem.fromUri(videoOneUri);
      exoPlayer.addMediaItem(mediaItem);
      exoPlayer.prepare();
    }
运行可以播放视频
https://i-blog.csdnimg.cn/blog_migrate/685f3802aec83278c85fc87982304d25.png
释放资源

媒体播放器是很占用资源的,所以当不再需要播放器时,要释放它,固然也要在Activity的生命周期中释放资源
https://i-blog.csdnimg.cn/blog_migrate/ea1da7cf4d00b3ffae12da07a54766d0.png
在onStop和onPause适当调用
https://i-blog.csdnimg.cn/blog_migrate/a12cce65d11bbd3e3ec3e0edc773431a.png
    @OptIn(markerClass = UnstableApi.class) @Override
    protected void onPause() {
      super.onPause();
      if (Util.SDK_INT < 24) {
            releasePlayer();
      }
    }

    @OptIn(markerClass = UnstableApi.class) @Override
    protected void onStop() {
      super.onStop();
      if (Util.SDK_INT >= 24) {
            releasePlayer();
      }
    }

    private void releasePlayer() {
      exoPlayer.release();
    }
记载视频的播放状态

重新运行项目,测试切换至后台
https://i-blog.csdnimg.cn/blog_migrate/1ebdd07d9795b6832b4ca9df3dcffa34.png
切换至后台的生命周期,可以知道当APP放到后台,会调用player.release()方法释放
https://i-blog.csdnimg.cn/blog_migrate/f88a004b05294b4e6dc80fda8d8b6dc6.png
我们发现视频切换到后台,再切换回来,之前的播放到第四秒,切换回来发现,又回到第0秒。所以,现在需要记载之前播放的位置,播放的状态,播放到第几个视频了。
既然没有onDestory掉Activity
那么定义三个变量(播放位置,播放状态,第几个视频)
    private Long playbackPosition = 0L;
    private int currentMediaItemIndex = 0;
    private boolean playWhenReady = false;
https://i-blog.csdnimg.cn/blog_migrate/f16c807f7851c74d6bc4f7e838a9a122.png
playbackPosition = exoPlayer.getCurrentPosition();
      currentMediaItemIndex = exoPlayer.getCurrentMediaItemIndex();
      playWhenReady = exoPlayer.getPlayWhenReady();
exoPlayer.setPlayWhenReady(playWhenReady);
      exoPlayer.seekTo(currentMediaItemIndex, playbackPosition);
添加新的视频链接
private final Uri videoTwoUri = Uri.parse("https://media.w3.org/2010/05/sintel/trailer.mp4");
https://i-blog.csdnimg.cn/blog_migrate/a6ede7130bf3ed9a615e70104e4f7caa.png
运行测试,播放下一个视频,并播放至一半,切换到后台,再切换回来
https://i-blog.csdnimg.cn/blog_migrate/92aa682ccaa5775bda590f23c17913b6.png
横竖屏切换

https://i-blog.csdnimg.cn/blog_migrate/fb43ee4196e5865545e7311839c3745c.png
横竖屏切换的生命周期,Activity被Destory了
https://i-blog.csdnimg.cn/blog_migrate/7dd72e8d6ee3b344a1a99233bda64c2f.png
使用SharedPreference来存储键值对,参考官方文档,官方推荐用DataStore
https://i-blog.csdnimg.cn/blog_migrate/58748f0895ac3f9073a3004f77004bbe.png
那使用DataStore
https://i-blog.csdnimg.cn/blog_migrate/77b6639ea4e631dba3ea93e825e98e1a.png
我们现在做的事情是,使用DataStore Preference来存储视频在释放之前的播放进度,播放状态,播放到第几个视频了。
按照官网设置Preferences DataStore
引入依赖
https://i-blog.csdnimg.cn/blog_migrate/4d834665d71180dc98262645238541c3.png
/// Preferences DataStore
    implementation("androidx.datastore:datastore-preferences:1.0.0")
    /// RxJava2 support
    implementation("androidx.datastore:datastore-preferences-rxjava2:1.0.0")
    /// RxJava3 support
    implementation("androidx.datastore:datastore-preferences-rxjava3:1.0.0")
实例RxDataStore,烧毁
private RxDataStore<Preferences> dataStore;
if (dataStore == null) {
            dataStore = new RxPreferenceDataStoreBuilder(this, "ExoPlayerKeys").build();
      }
    @Override
    protected void onDestroy() {
      super.onDestroy();
      if (dataStore != null) {
            dataStore.dispose();
      }
    }
https://i-blog.csdnimg.cn/blog_migrate/bc067efcf281d015838ec0ad68168fe0.png
https://i-blog.csdnimg.cn/blog_migrate/f9d3f158176997afeee0375d5a2b9838.png
定义三个Key
    Preferences.Key<Integer> currentMediaItemIndexPK;
    Preferences.Key<Integer> playbackPositionPK;
    Preferences.Key<Boolean> playWhenReadyPK;
currentMediaItemIndexPK = PreferencesKeys.intKey("currentMediaItemIndex");
      playbackPositionPK = PreferencesKeys.intKey("playbackPosition");
      playWhenReadyPK = PreferencesKeys.booleanKey("playWhenReady");
https://i-blog.csdnimg.cn/blog_migrate/0a78c33ada1d9b3bc1210c098d7c7398.png
使用PreferencesKeys存储值
https://i-blog.csdnimg.cn/blog_migrate/6f2cd08b2473b096b90894844742e5e2.png
private void saveVideoRecord() {
      dataStore.updateDataAsync(preferences -> {
            MutablePreferences mutablePreferences = preferences.toMutablePreferences();
            mutablePreferences.set(currentMediaItemIndexPK, currentMediaItemIndex);
            mutablePreferences.set(playbackPositionPK, playbackPosition.intValue());
            mutablePreferences.set(playWhenReadyPK, playWhenReady);
            return Single.just(mutablePreferences);
      });
    }
在onCreate()中取出之前存储的值,首次运行时,是没有存值的,所以会异常NULL,所以没有值就初始化这些变量
https://i-blog.csdnimg.cn/blog_migrate/608600cbaea4e70bd0ee5fa9718c3ff2.png
try {
            currentMediaItemIndex = dataStore.data().map(preferences -> preferences.get(currentMediaItemIndexPK)).blockingFirst();
      } catch (Exception e) {
            currentMediaItemIndex = 0;
      }
      try {
            playbackPosition = Long.valueOf(dataStore.data().map(preferences -> preferences.get(playbackPositionPK)).blockingFirst());
      } catch (Exception e) {
            playbackPosition = 0L;
      }
      try {
            playWhenReady = dataStore.data().map(preferences -> preferences.get(playWhenReadyPK)).blockingFirst();
      } catch (Exception e) {
            playWhenReady = false;
      }
删除APP,重新运行项目,旋转屏幕也能记载播放状态,和播放进度,播放第几个视频
https://i-blog.csdnimg.cn/blog_migrate/5f53122539a4eef84a94c13fdafa5a6e.png
https://i-blog.csdnimg.cn/blog_migrate/d92b6703d42f38bc484f0566bb97f5b2.png
旋转后,仍然从之前播放的进度继续,保持停息。
监听事件

参考链接
@OptIn(markerClass = UnstableApi.class)
    private void addPlayerListener() {
      listener = new Player.Listener() {
            @OptIn(markerClass = UnstableApi.class)
            @Override
            public void onPlaybackStateChanged(int playbackState) {
                Player.Listener.super.onPlaybackStateChanged(playbackState);
                String stateString = "UNKNOWN_STATE";
                if (playbackState == ExoPlayer.STATE_IDLE) {
                  stateString = "EXoPlayer.STATE_IDLE";
                } else if (playbackState == ExoPlayer.STATE_BUFFERING) {
                  stateString = "ExoPlayer.STATE_BUFFERING";
                } else if (playbackState == Player.STATE_READY) {
                  stateString = "ExoPlayer.STATE_READY";
                } else if (playbackState == Player.STATE_ENDED) {
                  stateString = "ExoPlayer.STATE_ENDED";
                }
                Log.d(TAG, "changed state to " + stateString);
            }
      };
      exoPlayer.addListener(listener);
      analyticsListener = new AnalyticsListener() {
            @Override
            public void onRenderedFirstFrame(EventTime eventTime, Object output, long renderTimeMs) {
                AnalyticsListener.super.onRenderedFirstFrame(eventTime, output, renderTimeMs);
                Log.i(TAG, "AnalyticsListener - onRenderedFirstFrame - 等待多长时间才能在屏幕上看到有意义的内容 - " + renderTimeMs);
            }

            @Override
            public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
                AnalyticsListener.super.onDroppedVideoFrames(eventTime, droppedFrames, elapsedMs);
                Log.i(TAG, "AnalyticsListener - onDroppedVideoFrames - 视频丢帧 - " + droppedFrames);
            }

            @Override
            public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
                AnalyticsListener.super.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
                Log.i(TAG, "AnalyticsListener - onAudioUnderrun - 音频欠载 - " + bufferSizeMs);
            }
      };
      exoPlayer.addAnalyticsListener(analyticsListener);
    }
自定义ExoPlayer界面

按住Command键,左键查看PlayerView.java,鼠标hover在文件上,看它的位置,可以顺藤摸瓜找到它的源码
https://i-blog.csdnimg.cn/blog_migrate/e9ced193f193302413ec3e8d8e84fb81.png
布局界面在这里
https://i-blog.csdnimg.cn/blog_migrate/64e8a4bbde1b787b3f0dd0740619d24e.png
复制到本身的项目中
https://i-blog.csdnimg.cn/blog_migrate/000b38f365fc90a6c4477cb466546157.png
在此基础上更改custom_player_control_view.xml
https://i-blog.csdnimg.cn/blog_migrate/2e9f524d3ec2bc506294a4fed523f766.png
https://i-blog.csdnimg.cn/blog_migrate/76922325d60077d476eaba77025caa78.png
MediaSessionService后台播放

参考文章
实现效果

让媒体挂在后台播放,如下图
https://i-blog.csdnimg.cn/blog_migrate/0edf09778add0467153fb4e865a735ce.png
正如官网所说,可以用在长视频,听视频。显然上面没有许多的交互,假如短视频用这个,既看不到播放的内容,也没啥交互,就还是不用MediaSession
https://i-blog.csdnimg.cn/blog_migrate/7496d4b420e42bf57b986ac2ab39908a.png
案例实现

按照官方设置的步调,语言还是采用Java。将构建一个MediaSession,让他在后台一直播放,同时可以自定义几个按钮,如点赞,收藏。末了监听视频是否播放。
MediaController

显然需要用到MediaSessionService和MediaController
https://i-blog.csdnimg.cn/blog_migrate/734b0ea859786b0993ea2af263e1ab9d.png
https://i-blog.csdnimg.cn/blog_migrate/951f15759ecc1d9f9120aa15d44224f1.png
创建项目

https://i-blog.csdnimg.cn/blog_migrate/703f41a9202cd4d0cde1ed25e6f42435.png
https://i-blog.csdnimg.cn/blog_migrate/5fe83675c986f7d60d1598e084ee62ca.png
按照步调,先new 一个ListenableFuture controllerFuture
先引入依赖
https://i-blog.csdnimg.cn/blog_migrate/fc26298d3f0a80b5f4646f3c20aad78c.png
https://i-blog.csdnimg.cn/blog_migrate/4ab734eb8ca69d8d403abab8c7155de6.png
    implementation ("androidx.media3:media3-session:1.3.1")
    implementation ("androidx.media3:media3-exoplayer:1.3.1")
    implementation ("androidx.media3:media3-ui:1.3.1")
    implementation ("androidx.media3:media3-common:1.3.1")
https://i-blog.csdnimg.cn/blog_migrate/9e808ffe1be42e37bdb0c69710c2d779.png
定义MediaController

https://i-blog.csdnimg.cn/blog_migrate/64751d6a52774e76e45b7bdc63109369.png
package com.test.mediasessionserviceexample;

import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken;

import android.content.ComponentName;
import android.net.Uri;
import android.os.Bundle;

import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;

import java.util.concurrent.ExecutionException;

public class MainActivity extends AppCompatActivity {

    ListenableFuture<MediaController> controllerFuture;
    private final String mediaUrl = "https://media.w3.org/2010/05/sintel/trailer.mp4";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      SessionToken sessionToken = new SessionToken(this, new ComponentName(this, 你的服务));
      controllerFuture = new MediaController.Builder(this, sessionToken).buildAsync();
      controllerFuture.addListener(() -> {
            Uri uri = Uri.parse(mediaUrl);
            MediaMetadata metadata = new MediaMetadata.Builder()
                  .setArtist("阿笙")
                  .setTitle("我名字是视频")
                  .build();
            MediaItem mediaItem = new MediaItem.Builder()
                  .setMediaId("media-one")
                  .setUri(uri)
                  .setMediaMetadata(metadata).build();
            try {
                MediaController mediaController = controllerFuture.get();
                mediaController.setMediaItem(mediaItem);
                mediaController.prepare();
            } catch (ExecutionException | InterruptedException e) {
                throw new RuntimeException(e);
            }
      }, MoreExecutors.directExecutor());
    }

    @Override
    protected void onStop() {
      super.onStop();
      MediaController.releaseFuture(controllerFuture);
    }
}
定义服务MediaSessionService

https://i-blog.csdnimg.cn/blog_migrate/65c6e3b7bc00afbb2e424e7a4fc715b3.png
package com.test.mediasessionserviceexample;

import android.content.Intent;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.Player;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.MediaSession;
import androidx.media3.session.MediaSession.ControllerInfo;
import androidx.media3.session.MediaSessionService;

public class PlaybackService extends MediaSessionService {
    private MediaSession mediaSession = null;

    @Override
    public void onCreate() {
      super.onCreate();
      ExoPlayer player = new ExoPlayer.Builder(this).build();
      mediaSession = new MediaSession.Builder(this, player).build();
    }

    @Nullable
    @Override
    public MediaSession onGetSession(@NonNull ControllerInfo controllerInfo) {
      return mediaSession;
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
      super.onTaskRemoved(rootIntent);
      Player player = mediaSession.getPlayer();
      if (!player.getPlayWhenReady() || player.getMediaItemCount() == 0 || player.getPlaybackState() == Player.STATE_ENDED) {
            stopSelf();
      }
    }

    @Override
    public void onDestroy() {
      super.onDestroy();
      mediaSession.getPlayer().release();
      mediaSession.release();
      mediaSession = null;
    }
}
https://i-blog.csdnimg.cn/blog_migrate/c8982f2f9751a17ab0bf3112c77a2c4e.png
注册服务/申明服务
<service android:name=".PlaybackService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="true">
            <intent-filter>
                <action android:name="androidx.media3.session.MediaSessionService"/>
            </intent-filter>
      </service>
申明权限(网络、前台服务)
<uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
https://i-blog.csdnimg.cn/blog_migrate/b508fa947fd268ecf4715a458288a077.png
运行项目

下拉通知,可以看到这个服务,
https://i-blog.csdnimg.cn/blog_migrate/d034128b992fd4a63fb4dd8c2229aa08.png
定义按钮CommandButton

引入图标
https://i-blog.csdnimg.cn/blog_migrate/271d72036d5bff30e06fc0c252e96c3b.png
https://i-blog.csdnimg.cn/blog_migrate/71d1717461ddae915093e10c4973c286.png
    private CommandButton cBRemoveFromLikes;
    private CommandButton cBRemoveFromFavorites;
    private CommandButton cBAddToLikes;
    private CommandButton cBAddToFavorites;
    private SessionCommand sCAddToLikes;
    private SessionCommand sCAddToFavorites;
    private SessionCommand sCRemoveFromLike;
    private SessionCommand sCRemoveFromFavorite;
    private static final String SAVE_TO_LIKES = "save to likes";
    private static final String SAVE_TO_FAVORITES = "save to favorites";
    private static final String REMOVE_FROM_LIKES = "remove from likes";
    private static final String REMOVE_FROM_FAVORITES = "remove from favorites";
sCAddToLikes = new SessionCommand(SAVE_TO_LIKES, new Bundle());
      sCAddToFavorites = new SessionCommand(SAVE_TO_FAVORITES, new Bundle());
      sCRemoveFromLike = new SessionCommand(REMOVE_FROM_LIKES, new Bundle());
      sCRemoveFromFavorite = new SessionCommand(REMOVE_FROM_FAVORITES, new Bundle());
      cBAddToLikes = buildCommandButton(SAVE_TO_LIKES, R.drawable.like_icon, sCAddToLikes);
      cBAddToFavorites = buildCommandButton(SAVE_TO_FAVORITES, R.drawable.favorite_icon, sCAddToFavorites);
      cBRemoveFromLikes = buildCommandButton(REMOVE_FROM_LIKES, R.drawable.like_remove_icon, sCRemoveFromLike);
      cBRemoveFromFavorites = buildCommandButton(REMOVE_FROM_FAVORITES, R.drawable.favorite_remove_icon, sCRemoveFromFavorite);
private CommandButton buildCommandButton(String displayName, int iconResId, SessionCommand sessionCommand) {
      return new CommandButton.Builder()
                .setDisplayName(displayName)
                .setIconResId(iconResId)
                .setSessionCommand(sessionCommand)
                .build();
    }
自定义MediaSession的布局

https://i-blog.csdnimg.cn/blog_migrate/40a38683b6366d465b8b64862cd346ef.png
定义MediaSession可以使用哪些自定义命令
在MediaSession.Builder()中setCallback 回调,setCustomLayout布局
https://i-blog.csdnimg.cn/blog_migrate/7a0fedbd7d08b8436d80491cb7abbf7c.png
通过吸收customAction的值,判断当前是什么命令,对应设置布局
https://i-blog.csdnimg.cn/blog_migrate/eee25b5a7b9f2e64bc5e834f0aa33fa2.png
class CustomMediaSessionCallback implements MediaSession.Callback {
      @OptIn(markerClass = UnstableApi.class)
      @NonNull
      @Override
      public ConnectionResult onConnect(@NonNull MediaSession session, @NonNull ControllerInfo controller) {
            SessionCommands sessionCommands = ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
                  .add(sCAddToLikes)
                  .add(sCAddToFavorites)
                  .add(sCRemoveFromLike)
                  .add(sCRemoveFromFavorite)
                  .build();

            return new ConnectionResult.AcceptedResultBuilder(session)
                  .setAvailableSessionCommands(sessionCommands)
                  .build();
      }

      @OptIn(markerClass = UnstableApi.class)
      @NonNull
      @Override
      public ListenableFuture<SessionResult> onCustomCommand(@NonNull MediaSession session, @NonNull ControllerInfo controller, @NonNull SessionCommand customCommand, @NonNull Bundle args) {
            final CommandButton buttonOne = mediaSession.getCustomLayout().get(0);
            final CommandButton buttonTwo = mediaSession.getCustomLayout().get(1);
            switch (customCommand.customAction) {
                case SAVE_TO_LIKES:
                  mediaSession.setCustomLayout(ImmutableList.of(cBRemoveFromLikes, buttonTwo));
                  break;
                case SAVE_TO_FAVORITES:
                  mediaSession.setCustomLayout(ImmutableList.of(buttonOne, cBRemoveFromFavorites));
                  break;
                case REMOVE_FROM_LIKES:
                  mediaSession.setCustomLayout(ImmutableList.of(cBAddToLikes, buttonTwo));
                  break;
                case REMOVE_FROM_FAVORITES:
                  mediaSession.setCustomLayout(ImmutableList.of(buttonOne, cBAddToFavorites));
                  break;
                default:
                  throw new RuntimeException("not implement");
            }
            return Futures.immediateFuture(new SessionResult(SessionResult.RESULT_SUCCESS));
      }
    }
运行测试

https://i-blog.csdnimg.cn/blog_migrate/6c3a04aba7f4d9605349e6acc9002d1f.png
自定义Player的行为(play等)

https://i-blog.csdnimg.cn/blog_migrate/af93f4fb38029eda3816bc7ff2c1f4f0.png
      ExoPlayer player = new ExoPlayer.Builder(this).build();

      ForwardingPlayer forwardingPlayer = new ForwardingPlayer(player) {
            @Override
            public void play() {
                super.play();
                Log.d("ForwardingPlayer Log", "play!");
            }
      };

      mediaSession = new MediaSession.Builder(this, forwardingPlayer)
                .setCallback(new CustomMediaSessionCallback())
                .setCustomLayout(ImmutableList.of(cBAddToLikes, cBAddToFavorites))
                .build();
MediaLibraryService

实现目标

参考文档
主要实现三个函数


[*]onGetLibraryRoot()
[*]onGetChildren()
[*]onGetSearchResult()
https://i-blog.csdnimg.cn/blog_migrate/45e67a2aa43942723a119bcfc1f4ab13.png
实现效果

创建一个目录,目录下有三个文件,并且可以根据标题搜索这三个文件
https://i-blog.csdnimg.cn/blog_migrate/4861b8d5e23edf20127d06cd9330c8e8.png
案例实现

采用JAVA实现
创建项目接着引入依赖

https://i-blog.csdnimg.cn/blog_migrate/28e069528a57a4571aa151525a1cd3df.png
    implementation ("androidx.media3:media3-session:1.3.1")
    implementation ("androidx.media3:media3-exoplayer:1.3.1")
    implementation ("androidx.media3:media3-ui:1.3.1")
    implementation ("androidx.media3:media3-common:1.3.1")
初始化MediaLibraryService

https://i-blog.csdnimg.cn/blog_migrate/27182c21df0c872f1b2d7b8d86c1727c.png
https://i-blog.csdnimg.cn/blog_migrate/c8b313398333f159e2f6901460596794.png
和MediaSession利用差不多,先实例
https://i-blog.csdnimg.cn/blog_migrate/a91fc9cd0f805bb2df506ba1080e528c.png
package com.test.medialibraryservicetestapplication;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.session.LibraryResult;
import androidx.media3.session.MediaLibraryService;
import androidx.media3.session.MediaSession;

import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.ListenableFuture;

public class PlaybackService extends MediaLibraryService {
    MediaLibrarySession mediaLibrarySession = null;
    MediaLibrarySession.Callback callback = new MediaLibrarySession.Callback() {
      @NonNull
      @Override
      public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRoot(@NonNull MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @Nullable MediaLibraryService.LibraryParams params) {
            return MediaLibrarySession.Callback.super.onGetLibraryRoot(session, browser, params);
      }

      @NonNull
      @Override
      public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String parentId, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) {
            return MediaLibrarySession.Callback.super.onGetChildren(session, browser, parentId, page, pageSize, params);
      }

      @NonNull
      @Override
      public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetSearchResult(@NonNull MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String query, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) {
            return MediaLibrarySession.Callback.super.onGetSearchResult(session, browser, query, page, pageSize, params);
      }
    };

    @Nullable
    @Override
    public MediaLibrarySession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {
      return mediaLibrarySession;
    }

    @Override
    public void onCreate() {
      super.onCreate();
      ExoPlayer player = new ExoPlayer.Builder(this).build();
      mediaLibrarySession = new MediaLibraryService.MediaLibrarySession.Builder(this, player, callback).build();
    }

    @Override
    public void onDestroy() {
      super.onDestroy();
      if (mediaLibrarySession != null) {
            mediaLibrarySession.getPlayer().release();
            mediaLibrarySession.release();
            mediaLibrarySession = null;
      }
    }
}
创建树形结构存储数据

要获取根对象,需要有一个建好的树形数据,就如下面这幅图上,它是一个树,树的根节点RootNode,分叉出去好几个类目,简单起见,这里先弄简单的Root->Music/Game/Study
https://i-blog.csdnimg.cn/blog_migrate/51ef0d6dff31bc51dedb845656ee112f.png
定义树
https://i-blog.csdnimg.cn/blog_migrate/adeb14c92df1868f6b5ab162b1d085e6.png
构建根节点,添加三个子节点到根节点
https://i-blog.csdnimg.cn/blog_migrate/ad00932c41e06e7ac3593c07fba67b6d.png
末了改为单例
package com.test.medialibraryservicetestapplication;

import java.util.HashMap;
import java.util.Map;

public class Tree {
    Map<String, TreeNode> treeNodes;
    final String ROOT_ID = "root";
    final String MUSIC_ID = "music";
    final String GAME_ID = "game";
    final String STUDY_ID = "study";
    final String ROOT_TITLE = "root title";
    final String MUSIC_TITLE = "music title";
    final String GAME_TITLE = "game title";
    final String STUDY_TITLE = "study title";
    private final static Tree instance = new Tree();

    public static Tree genInstance() {
      return instance;
    }
    private boolean isInitialized = false;
    private Tree() {
      if (!isInitialized) {
            isInitialized = true;
            treeNodes = new HashMap<>();
            TreeNode rootNode = new TreeNode(ROOT_ID, ROOT_TITLE);
            TreeNode musicNode = new TreeNode(MUSIC_ID, MUSIC_TITLE);
            TreeNode gameNode = new TreeNode(GAME_ID, GAME_TITLE);
            TreeNode studyNode = new TreeNode(STUDY_ID, STUDY_TITLE);
            rootNode.children.add(musicNode.node);
            rootNode.children.add(gameNode.node);
            rootNode.children.add(studyNode.node);
            treeNodes.put(ROOT_ID, rootNode);
      }

    }
}
获取根节点

https://i-blog.csdnimg.cn/blog_migrate/f883853fc2cbf383668a3f655c8d2bbd.png
MediaItem getTreeRoot() {
      return Objects.requireNonNull(treeNodes.get(ROOT_ID)).node;
    }
      @NonNull
      @Override
      public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRoot(@NonNull MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @Nullable MediaLibraryService.LibraryParams params) {
            return Futures.immediateFuture(LibraryResult.ofItem(Tree.genInstance().getTreeRoot(), params));
      }
获取结点的子节点集合

https://i-blog.csdnimg.cn/blog_migrate/314ac42a3aa777480d07769e7a76e8c4.png
    List<MediaItem> getNodeChildren(String treeNodeId) {
      return Objects.requireNonNull(treeNodes.get(treeNodeId)).children;
    }
@NonNull
      @Override
      public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String parentId, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) {
            List<MediaItem> children = Tree.genInstance().getNodeChildren(parentId);
            if (!children.isEmpty()) {
                return Futures.immediateFuture(LibraryResult.ofItemList(children, params));
            }
            return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
      }
注册Service及申明权限

https://i-blog.csdnimg.cn/blog_migrate/4c764cf6536c93332f508dcfea946217.png
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<service
            android:name=".PlaybackService"
            android:exported="true"
            android:foregroundServiceType="mediaPlayback">
            <intent-filter>
                <action android:name="androidx.media3.session.MediaSessionService" />
            </intent-filter>
      </service>
MediaBrowser浏览MediaLibraryService定义的媒体库

https://i-blog.csdnimg.cn/blog_migrate/b6bd5952dc1cd760097ff2087b752575.png
https://i-blog.csdnimg.cn/blog_migrate/72480a07ad763c221fba8071e5e523c7.png
获取MediaLibraryService构建的媒体库的根节点
https://i-blog.csdnimg.cn/blog_migrate/9dd7fd49465ccce36eab35f736fcb238.png
获取某一节点下的子节点
https://i-blog.csdnimg.cn/blog_migrate/0ec5fd72de6919c33d94152cab4ee15e.png
运行测试

运行项目,并在60行打上断点,看到value属性值为三个MediaItem对象
可以看到获取到根节点下的三个子节点
https://i-blog.csdnimg.cn/blog_migrate/7956c32f0d5d301e4422302096703581.png#pic_center
package com.test.medialibraryservicetestapplication;import androidx.annotation.NonNull;import androidx.annotation.Nullable;import androidx.media3.common.MediaItem;import androidx.media3.exoplayer.ExoPlayer;import androidx.media3.session.LibraryResult;import androidx.media3.session.MediaLibraryService;import androidx.media3.session.MediaSession;import com.google.common.collect.ImmutableList;import com.google.common.util.concurrent.Futures;import com.google.common.util.concurrent.ListenableFuture;import java.util.List;public class PlaybackService extends MediaLibraryService {    MediaLibrarySession mediaLibrarySession = null;    MediaLibrarySession.Callback callback = new MediaLibrarySession.Callback() {      @NonNull
      @Override
      public ListenableFuture<LibraryResult<MediaItem>> onGetLibraryRoot(@NonNull MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @Nullable MediaLibraryService.LibraryParams params) {
            return Futures.immediateFuture(LibraryResult.ofItem(Tree.genInstance().getTreeRoot(), params));
      }
      @NonNull
      @Override
      public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetChildren(MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String parentId, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) {
            List<MediaItem> children = Tree.genInstance().getNodeChildren(parentId);
            if (!children.isEmpty()) {
                return Futures.immediateFuture(LibraryResult.ofItemList(children, params));
            }
            return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE));
      }
      @NonNull      @Override      public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetSearchResult(@NonNull MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String query, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) {            return MediaLibrarySession.Callback.super.onGetSearchResult(session, browser, query, page, pageSize, params);      }    };    @Nullable    @Override    public MediaLibrarySession onGetSession(@NonNull MediaSession.ControllerInfo controllerInfo) {      return mediaLibrarySession;    }    @Override    public void onCreate() {      super.onCreate();      ExoPlayer player = new ExoPlayer.Builder(this).build();      mediaLibrarySession = new MediaLibraryService.MediaLibrarySession.Builder(this, player, callback).build();    }    @Override    public void onDestroy() {      super.onDestroy();      if (mediaLibrarySession != null) {            mediaLibrarySession.getPlayer().release();            mediaLibrarySession.release();            mediaLibrarySession = null;      }    }} 搜索媒体库

首先还是去Tree中定义搜索的函数
https://i-blog.csdnimg.cn/blog_migrate/463836b15b759bdcd64caaf9b42159ea.png
    List<MediaItem> search(String query) {
      List<MediaItem> titleMatches = new ArrayList<>();
      Object[] words = Arrays.stream(query.split(" ")).map(it -> it.trim().toLowerCase()).filter(it -> it.length() > 1).toArray();
      titleNodes.keySet().forEach(title -> {
            TreeNode treeNode = titleNodes.get(title);
            for (Object word : words) {
                boolean contains = title.contains((CharSequence) word);
                if (contains) {
                  assert treeNode != null;
                  titleMatches.add(treeNode.node);
                }
            }
      });
      return titleMatches;
    }
    public static Map<String, TreeNode> titleNodes;
titleNodes = new HashMap<>();
            titleNodes.put(MUSIC_TITLE, musicNode);
            titleNodes.put(GAME_TITLE, gameNode);
            titleNodes.put(STUDY_TITLE, studyNode);
https://i-blog.csdnimg.cn/blog_migrate/76a0a475945ec1a4f5bc9038a717cd76.png
      @NonNull
      @Override
      public ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> onGetSearchResult(@NonNull MediaLibrarySession session, @NonNull MediaSession.ControllerInfo browser, @NonNull String query, int page, int pageSize, @Nullable MediaLibraryService.LibraryParams params) {
            return Futures.immediateFuture(LibraryResult.ofItemList(Tree.genInstance().search(query), params));
      }
MediaBrowser 搜索

    private void searchMedia(String query) {
      ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> searchFuture = mediaBrowser.getSearchResult(query, 0, Integer.MAX_VALUE, null);
      searchFuture.addListener(() -> {
            try {
                LibraryResult<ImmutableList<MediaItem>> immutableListLibraryResult = searchFuture.get();
                ImmutableList<MediaItem> value = immutableListLibraryResult.value;
            } catch (ExecutionException | InterruptedException e) {
                throw new RuntimeException(e);
            }
      }, MoreExecutors.directExecutor());
    }
https://i-blog.csdnimg.cn/blog_migrate/7639f3e34dd2231182690320e45cab25.png
搜索“ga”,可以查到“game”这个MediaItem
https://i-blog.csdnimg.cn/blog_migrate/079ae7216d4b4f8373667408da7ad36b.png#pic_center
填坑中…

内容是真的多!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【重生之我在学Android原生】Media3