How to download a video while playing it, using ExoPlayer?

I took a look at erdemguven's sample code here and seem to have something that works. This is by-and-large what erdemguven wrote, but I write to a file instead of a byte array and create the data source. I am thinking that since erdemguven, who is an ExoPlayer expert, presented this as the correct way to access cache, that my mods are also "correct" and do not break any rules.

Here is the code. getCachedData is the new stuff.

class MainActivity : AppCompatActivity(), CacheDataSource.EventListener, TransferListener {

    private var player: SimpleExoPlayer? = null

    companion object {
        // About 10 seconds and 1 meg.
//        const val VIDEO_URL = "https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_1mb.mp4"

        // About 1 minute and 5.3 megs
        const val VIDEO_URL = "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"

        // The full movie about 355 megs.
//        const val VIDEO_URL = "http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_60fps_normal.mp4"

        // Use to download video other than the one you are viewing. See #3 test of the answer.
//        const val VIDEO_URL_LIE = "http://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_480_1_5MG.mp4"

        // No changes in code deleted here.

    //NOTE: I know I shouldn't use an AsyncTask. It's just a sample...
    @SuppressLint("StaticFieldLeak")
    fun tryShareCacheFile() {
        // file is cached and ready to be used
        object : AsyncTask<Void?, Void?, File>() {
            override fun doInBackground(vararg params: Void?): File {
                val tempFile = FilesPaths.FILE_TO_SHARE.getFile(this@MainActivity, true)
                getCachedData(this@MainActivity, cache, VIDEO_URL, tempFile)
                return tempFile
            }

            override fun onPostExecute(result: File) {
                super.onPostExecute(result)
                val intent = prepareIntentForSharingFile(this@MainActivity, result)
                startActivity(intent)
            }
        }.execute()
    }

    private var mTotalBytesToRead = 0L
    private var mBytesReadFromCache: Long = 0
    private var mBytesReadFromNetwork: Long = 0

    @WorkerThread
    fun getCachedData(
        context: Context, myCache: Cache?, url: String, tempfile: File
    ): Boolean {
        var isSuccessful = false
        val myUpstreamDataSource = DefaultHttpDataSourceFactory(ExoPlayerEx.getUserAgent(context)).createDataSource()
        val dataSource = CacheDataSource(
            myCache,
            // If the cache doesn't have the whole content, the missing data will be read from upstream
            myUpstreamDataSource,
            FileDataSource(),
            // Set this to null if you don't want the downloaded data from upstream to be written to cache
            CacheDataSink(myCache, CacheDataSink.DEFAULT_BUFFER_SIZE.toLong()),
            /* flags= */ 0,
            /* eventListener= */ this
        )

        // Listen to the progress of the reads from cache and the network.
        dataSource.addTransferListener(this)

        var outFile: FileOutputStream? = null
        var bytesRead = 0

        // Total bytes read is the sum of these two variables.
        mTotalBytesToRead = C.LENGTH_UNSET.toLong()
        mBytesReadFromCache = 0
        mBytesReadFromNetwork = 0

        try {
            outFile = FileOutputStream(tempfile)
            mTotalBytesToRead = dataSource.open(DataSpec(Uri.parse(url)))
            // Just read from the data source and write to the file.
            val data = ByteArray(1024)

            Log.d("getCachedData", "<<<<Starting fetch...")
            while (bytesRead != C.RESULT_END_OF_INPUT) {
                bytesRead = dataSource.read(data, 0, data.size)
                if (bytesRead != C.RESULT_END_OF_INPUT) {
                    outFile.write(data, 0, bytesRead)
                }
            }
            isSuccessful = true
        } catch (e: IOException) {
            // error processing
        } finally {
            dataSource.close()
            outFile?.flush()
            outFile?.close()
        }

        return isSuccessful
    }

    override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
        Log.d("onCachedBytesRead", "<<<<Cache read? Yes, (byte read) $cachedBytesRead (cache size) $cacheSizeBytes")
    }

    override fun onCacheIgnored(reason: Int) {
        Log.d("onCacheIgnored", "<<<<Cache ignored. Reason = $reason")
    }

    override fun onTransferInitializing(source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean) {
        Log.d("TransferListener", "<<<<Initializing isNetwork=$isNetwork")
    }

    override fun onTransferStart(source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean) {
        Log.d("TransferListener", "<<<<Transfer is starting isNetwork=$isNetwork")
    }

    override fun onTransferEnd(source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean) {
        reportProgress(0, isNetwork)
        Log.d("TransferListener", "<<<<Transfer has ended isNetwork=$isNetwork")
    }

    override fun onBytesTransferred(
        source: DataSource?,
        dataSpec: DataSpec?,
        isNetwork: Boolean,
        bytesTransferred: Int
    ) {
        // Report progress here.
        if (isNetwork) {
            mBytesReadFromNetwork += bytesTransferred
        } else {
            mBytesReadFromCache += bytesTransferred
        }

        reportProgress(bytesTransferred, isNetwork)
    }

    private fun reportProgress(bytesTransferred: Int, isNetwork: Boolean) {
        val percentComplete =
            100 * (mBytesReadFromNetwork + mBytesReadFromCache).toFloat() / mTotalBytesToRead
        val completed = "%.1f".format(percentComplete)
        Log.d(
            "TransferListener", "<<<<Bytes transferred: $bytesTransferred isNetwork=$isNetwork" +
                    " $completed% completed"
        )
    }

    // No changes below here.
}

Here is what I did to test this and this is by no means exhaustive:

  1. Simply shared through email the video using the FAB. I received the video and was able to play it.
  2. Turned off all network access on a physical device (airplane mode = on) and shared the video via email. When I turned the network back on (airplane mode = off), I received and was able to play the video. This shows that the video had to come from cache since the network was not available.
  3. Changed the code so that instead of VIDEO_URL being copied from cache, I specified that VIDEO_URL_LIE should be copied. (The app still played only VIDEO_URL.) Since I had not downloaded the video for VIDEO_URL_LIE, the video was not in cache, so the app had to go out to the network for the video. I successfully received the correct video though email and was able to play it. This shows that the app can access the underlying asset if cache is not available.

I am by no means an ExoPlayer expert, so you will be able to stump me quickly with any questions that you may have.


The following code will track progress as the video is read and stored in a local file.

// Get total bytes if known. This is C.LENGTH_UNSET if the video length is unknown.
totalBytesToRead = dataSource.open(DataSpec(Uri.parse(url)))

// Just read from the data source and write to the file.
val data = ByteArray(1024)
var bytesRead = 0
var totalBytesRead = 0L
while (bytesRead != C.RESULT_END_OF_INPUT) {
    bytesRead = dataSource.read(data, 0, data.size)
    if (bytesRead != C.RESULT_END_OF_INPUT) {
        outFile.write(data, 0, bytesRead)
        if (totalBytesToRead == C.LENGTH_UNSET.toLong()) {
            // Length of video in not known. Do something different here.
        } else {
            totalBytesRead += bytesRead
            Log.d("Progress:", "<<<< Percent read: %.2f".format(totalBytesRead.toFloat() / totalBytesToRead))
        }
    }
}