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.