Android dynamic agent mode realizes a player that can listen and save

Keywords: Android

Key points of text objectives

The development of audio and video is very popular now. Playing online audio and video on the mobile terminal is a very traffic consuming scene. Therefore, a good player should save while listening, keep the buffer margin relative to the user's current playback progress, but avoid buffering all files at one time, and recover the cache when the buffer margin is insufficient. There are many examples of player design. This article will not explain how to develop the player, but focus on how to realize cache control based on dynamic agent.

Dynamic agent concept

According to the definition in Java programming ideas, agent is one of the basic design patterns. It is an object you insert to replace the "actual" object in order to provide additional or different modules. The agent usually acts as an intermediary. Dynamic proxy can dynamically create proxy and dynamically handle the call to the proxy method.

Basic framework of the scheme

A MediaPlayer that supports direct loading and playing of network audio, as well as playing of local audio files. When setting the playback audio, the setDataSource method of MediaPlay will be called to input the network audio url. In this paper, based on the dynamic agent, the network audio will be gradually cached to the local hard disk. After the playback conditions are met, the url of the local audio will be transmitted to the setDataSource, and part of the cached data can be downloaded in real time according to the playback progress.

1. Main composition

ControllerService: a service for playback control, receiving control commands, sending playback information, and controlling agents
AudioPlayer: encapsulates the MediaPlayer player, plays audio resources, and regularly notifies the playback progress
AudioCacheHandler: implements a proxy for some methods of AudioPlayer
AudioCache: the cache control module converts the network audio url into the local audio url and feeds it back to the player; Receive the playback progress, and start the recovery download when it is found that the cache margin is insufficient according to the playback progress; Monitor the cache progress and stop downloading when it is found that the cache margin is sufficient to ensure the playback function
FileDownload: the download function module provides download and pause functions, supports timely maintaining the download progress status, and supports reading the download progress status for breakpoint continuous transmission

Main process description

1. Dynamic proxy interception entry

The purpose of using dynamic proxy is to convert the network audio url to the local audio file path. Therefore, the method of playing network audio will be set as the entry point of dynamic proxy. The basic process is as follows

2. Dynamic agent implementation class

In the dynamic proxy implementation class AudioCacheHandler, the play method will be intercepted, in which the cache control module will be called, the network audio url in the input parameter will be converted to the url of the local audio file, and the file download task will be started to block the current thread.

3. Cache control module logic

The cache control module will feed back the local url path of the downloaded audio based on the set rules. Before the local url path of the audio cannot meet the playback conditions, the thread will be blocked, and the download module will be called to download the file; After the download progress meets the playback conditions, the blocking will be released and the player will be notified that the cache is complete. The cache module can obtain the current playback progress from the dynamic proxy class. According to the playback progress analysis, when the cache margin is insufficient, the cache will be restored; At the same time, monitor the cache progress of the download module. When the cache margin is enough to play, the cache will be stopped to avoid excessive traffic consumption.

4. Download module

It provides download and pause functions, supports timely maintenance of download progress status, and supports reading download progress status for breakpoint continuous transmission. Its basic functions are as follows

Introduction to main codes

1. Implementation class of dynamic agent

The InvocationHandler interface needs to be implemented to intercept the play method, play progress and release player resources

/**
 * Proxy implementation class
 * Intercept the play method and convert the network audio url to the local audio url
 * In order to save traffic, the target cache location can only be larger than the playback location by a certain margin
 */
public class AudioCacheHandler implements InvocationHandler {
    private static final String TAG = "AudioCacheHandler";
    private Object mObject;
    private AudioCache mAudioCache = new AudioCache();

    public AudioCacheHandler(Object object) {
        mObject = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.d(TAG, "proxy = "+proxy.getClass().getName()+",method name = "+method.getName());
        //Judge whether to execute cache logic and cache listening when the setDataSource method is executed
        if(method.getName().equals("play")){
            //Register listening cache progress for Audio
            mAudioCache.setBufferStatus(((IPlayerFactory)proxy).onBufferListener());
            //Convert network url to local path
            for(int i = 0; i < args.length;i++){
                Object arg = args[i];
                if((arg instanceof PlayInfoBean) && (((PlayInfoBean)arg).getUrl()).contains("http")){
                    String localUrl = mAudioCache.reviseDataSource(((PlayInfoBean)arg).getUrl().toString());
                    ((PlayInfoBean)arg).setUrl(localUrl);
                    Log.d(TAG, "reset data source");
                    args[i] = arg;
                    break;
                }
            }
        }else if(method.getName().equals("onProgressRatio")){
            //When judging the player's update progress, write the progress information into AudioCache to match the appropriate buffer location
            if(proxy instanceof IPlayerFactory){
                for(int i = 0; i < args.length;i++){
                    if(args[i] instanceof Float){
                        mAudioCache.setProgress((Float) args[i]);
                        break;
                    }
                }
            }
        }else if(method.getName().equals("release")){
            //When the listening player is ready to release, remove the internal listening object of AudioCache to avoid memory leakage
            if(proxy instanceof IPlayerFactory){
                mAudioCache.removeBufferStatus();
            }
        }
        //Execute original method
        Object result = method.invoke(this.mObject, args);
        return result;
    }
}

Create a dynamic proxy in the service of playback control

//Proxied object
AudioPlayer audioPlayer = new AudioPlayer();
//Create InvocationHandler
AudioCacheHandler invocationHandler = new AudioCacheHandler(audioPlayer);
//Proxy object
mAudioPlayerProxy = (IPlayerFactory)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), audioPlayer.getClass().getInterfaces(), invocationHandler);

2. Cache control module code

The function of this part is to judge when the download should be resumed and when the download should be suspended. The conversion of the network url to the local path is also completed here.

/**
 * Audio files are saved while listening < P / >
 * According to the playback progress, when it is found that the cache margin is insufficient, resume the download action < P / >
 * Monitor the download progress and stop the download action when it is found that the download margin is sufficient < P / >
 */
public class AudioCache implements IDownloadListener{
    //Local audio cache path
    private static final String LOCAL_FILE_DIR = "/data/data/" + AudioCache.class.getPackage().getName()+"/cache";
    //Current playback progress
    private float mProgress;
    //Advance cache factor, cache exceeding 15% of playback progress
    private static final float PRE_CACHE_RATIO = 0.15f;
    //Insufficient cache factor
    private static final float LOW_CACHE_RATIO = 0.05f;
    //The current download pause flag bit. true means downloading and false means stopping downloading
    private boolean mDownloadRunning = true;
    //Interface to notify cache progress
    private IBufferStatus mBufferStatus;
    //When the cache progress reaches the minimum threshold, the player will be notified of ready, and only once for a single download;
    private boolean mReadyFlag = false;
    private Object mBlockObject = new Object();
    //Download task
    private FileDownload mCurrentTask;
    private static final String TAG = "AudioCache";
    /**
     * Internal processing logic, first find whether there are local records
     * If there is a record, directly convert the network url to the local path
     * If there is no record, execute the download action
     * @param url Audio network link
     */
    public String reviseDataSource(@NonNull String url) {
        Log.d(TAG, "revise audio cache");
        //Find the local url link
        String localUrl = checkLocalSource(url);
        //If there is no local record, perform the download
        String urlFileName = url.substring(url.lastIndexOf("/")+1);
        String localPath = LOCAL_FILE_DIR + "/" + urlFileName;
        mCurrentTask = new FileDownload.Builder().setUrl(url).setLocalPath(localPath).setDownloadListener(AudioCache.this).build();
        mCurrentTask.start();
        synchronized (mBlockObject){
            try{
                mBlockObject.wait(5000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        Log.d(TAG,"thread notified");
        return "file://" + localPath;
    }

    /**
     * Find local cache records based on network url
     * @param url Network audio link
     * @return Local audio file link
     */
    private String checkLocalSource(String url){
        String urlFileName = url.substring(url.lastIndexOf("/")+1);
        File dir = new File(LOCAL_FILE_DIR);
        if(!dir.exists()){
            dir.mkdir();
        }
        File file = new File(LOCAL_FILE_DIR + "/" + urlFileName);
        if(file.exists()){
            //Local records already exist
            return file.getAbsolutePath();
        }
        return null;
    }

    @Override
    public void paused(int soFarBytes, int totalBytes) {
        //suspend
        mDownloadRunning = false;
    }

    /**
     * Monitor the current download progress < P / >
     * It is necessary to control the cache progress not more than 30s before the playback progress, mainly to decide to stop downloading
     * @param soFarBytes Current cache size
     * @param totalBytes Total file size
     */
    @Override
    public void progress(int soFarBytes, int totalBytes) {
        Log.d(TAG, "download process audio cache sofar bytes = "+soFarBytes+", totalBytes = "+totalBytes+" , mProgress = "+mProgress);
        onBufferingProgress(1.00f * soFarBytes / totalBytes);
        float hopeCache = (mProgress + LOW_CACHE_RATIO)*totalBytes;
        Log.d(TAG, "hope cache = "+hopeCache+", cache is not enough = "+(hopeCache > soFarBytes)+", ready flag = "+mReadyFlag);
        if(totalBytes == soFarBytes && soFarBytes > 0){
            Log.d(TAG, "has local memory and file is completed");
            if(mProgress <= 0){
                mBlockObject.notifyAll();
            }
            mBufferStatus.onBufferReady();
            mReadyFlag = true;
            return;
        }else if(((mProgress + PRE_CACHE_RATIO)*1.00f*totalBytes <= soFarBytes) && (soFarBytes < totalBytes)){
            //1. Execute stop when the cache margin is sufficient
            Log.d(TAG,"task pause");
            mCurrentTask.pause();
            if(mProgress <= 0 ){
                Log.d(TAG, "block object notify");
                mBlockObject.notifyAll();
            }
            mBufferStatus.onBufferReady();
            mReadyFlag = true;
        }
    }

    @Override
    public void error(Throwable e) {

    }

    @Override
    public void completed() {
        Log.d(TAG, "audio cache complete");
        onBufferingProgress(1);
        mBufferStatus.onBufferReady();
    }

    /**
     * Listen to the current track playing percentage < P / >
     * It is used to decide whether to continue loading < P / >
     * @param progress
     */
    public void setProgress(float progress) {
        Log.d(TAG , "play progress = "+progress+" , " +
                "current bytes = "+mCurrentTask.getSmallFileSoFarBytes()+", " +
                "total bytes = "+mCurrentTask.getSmallFileTotalBytes());
        mProgress = progress;
        if(mCurrentTask.getSmallFileTotalBytes() <= mCurrentTask.getSmallFileSoFarBytes()){
            return;
        }
        if(((mProgress + LOW_CACHE_RATIO)*mCurrentTask.getSmallFileTotalBytes() > mCurrentTask.getSmallFileSoFarBytes())
                && (mCurrentTask.getSmallFileTotalBytes() > mCurrentTask.getSmallFileSoFarBytes())){
            //The cache is already low
            Log.d(TAG, "cache is not enough");
            if(!mCurrentTask.isRunning()){
                Log.d(TAG, "notice on buffering wait");
                mCurrentTask.start();
                onBufferingWait();
            }
        }
    }

    public void setBufferStatus(IBufferStatus bufferStatus) {
        Log.d(TAG,"set buffer status = "+bufferStatus.getClass().getName());
        mBufferStatus = bufferStatus;
        mProgress = 0;
    }

    public void removeBufferStatus() {
        mBufferStatus = null;
    }

    private void onBufferingProgress(float progress){
        if(null != mBufferStatus){
            mBufferStatus.onBuffering(progress);
        }
    }

    private void onBufferingWait(){
        if(null != mBufferStatus){
            mBufferStatus.onBufferWait();
        }
    }
}

3. Download module code

The download module internally accesses the network based on HttpUrlConnection, and internally realizes breakpoint continuous transmission according to the last download location maintained.

/**
 * The logic of resuming the download cannot resume the copying of stream content by removing thread blocking, so thread blocking does not need to be set for the whole suspended work < P / >
 * To resume the download, you need to re execute the HttpUrlConnection connection, just set the download from the memorized location < P / >
 */
public void start(){
    Log.d(TAG , "start task");
    reset();
    String name = mUrl.substring(mUrl.lastIndexOf("/")+1);
    File file = new File(localPath);
    if(mMMKV.containsKey(name) && file.exists()){
        ProgressMemoryBean memoryBean = JSON.parseObject(mMMKV.decodeString(name), ProgressMemoryBean.class);
        if(memoryBean.mDownloadFinished){
            //It has been recorded and downloaded
            Log.d(TAG,"has record and download finished");
            totalBytes = memoryBean.mDownloadProgress;
            sofarBytes = totalBytes;
            mDownloadListener.progress(sofarBytes, totalBytes);
            mDownloadListener.completed();
            return;
        }else{
            //Incomplete download
            Log.d(TAG,"has record but download not finished");
            sofarBytes = memoryBean.mDownloadProgress;
        }
    }
    Schedulers.computation().scheduleDirect(new Runnable() {
        @Override
        public void run() {
            mActionThread = Thread.currentThread();
            Log.d(TAG, "create thread , state = "+mActionThread.getState());
            try{
                URL url = new URL(mUrl);
                HttpURLConnection connection = (HttpURLConnection)url.openConnection();
                connection.setConnectTimeout(5*1000);
                connection.setRequestMethod("GET");
                long fileSize = connection.getContentLength();
                totalBytes = (int)fileSize;
                //Adjust the write position to the last download position
                RandomAccessFile localFile = new RandomAccessFile(file, "rw");
                localFile.setLength(fileSize);
                Log.d(TAG,"song name = "+name+",total size is "+totalBytes+", last file size = "+sofarBytes+",access file size = "+localFile.length());
                localFile.seek(sofarBytes);
                //Download File
                writeData(connection, localFile, sofarBytes);
                connection.disconnect();
            }catch (MalformedURLException e){
                mDownloadListener.error(new Throwable("MalformedURLException"));
                e.printStackTrace();
            }catch (IOException e){
                e.printStackTrace();
                mDownloadListener.error(new Throwable("IOException"));
            }finally {
                mMMKV.putString(name, JSON.toJSONString(mProgressMemoryBean));
            }
        }
    });
}

Supports code fragments that are suspended from downloading. When the downloading is suspended, the cycle of copying data will jump out and the connection will be ended

while ((hasRead = inputStream.read(buffer)) > 0){
    file.write(buffer, 0, hasRead);
    hasLength += hasRead;
    //Control the frequency of progress notification
    if(hasLength - sofarBytes > 1024*100){
        mDownloadListener.progress(hasLength , totalBytes);
        sofarBytes = hasLength;
        mProgressMemoryBean.mDownloadProgress = sofarBytes;
        Log.d(TAG, "total length = "+totalBytes+",download length = "+hasLength);
    }
    //Because of pause interrupt
    if(!mAction){
        sofarBytes = hasLength;
        mProgressMemoryBean.mDownloadProgress = sofarBytes;
        Log.d(TAG,"download block set file length = "+sofarBytes);
        mDownloadListener.paused(sofarBytes , totalBytes);
        break;
    }
}

Effect display

It can achieve better state following of caching and playback
Blue indicates cache progress
Purple is the playback progress

Learning experience

Although the scene of listening while saving described in this paper is developed based on the playback of online audio, it is more meaningful for the playback of online video in a practical sense. For the scene of playing video, the above code and logic can also be fully applicable. Due to the author's limited ability, there may be mistakes. Welcome to communicate.

Posted by biggieuk on Sat, 20 Nov 2021 11:33:20 -0800