Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Background

I'm working on an app that can play some short videos.

I want to avoid accessing the Internet every time the user plays them, to make it faster and to lower the data usage.

The problem

Currently I've only found how to either play or download (it's just a file so I could download it like any other file).

Here's the code of playing a video file from URL (sample available here):

gradle

...
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.exoplayer:exoplayer-core:2.8.4'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.8.4'
...

manifest

<manifest package="com.example.user.myapplication" xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"
        tools:ignore="AllowBackup,GoogleAppIndexingWarning">
        <activity
            android:name=".MainActivity" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

activity_main.xml

<FrameLayout
    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">

    <com.google.android.exoplayer2.ui.PlayerView
        android:id="@+id/playerView" android:layout_width="match_parent" android:layout_height="match_parent"
        app:resize_mode="zoom"/>
</FrameLayout>

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private var player: SimpleExoPlayer? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onStart() {
        super.onStart()
        playVideo()
    }

    private fun playVideo() {
        player = ExoPlayerFactory.newSimpleInstance(this@MainActivity, DefaultTrackSelector())
        playerView.player = player
        player!!.addVideoListener(object : VideoListener {
            override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) {
            }

            override fun onRenderedFirstFrame() {
                Log.d("appLog", "onRenderedFirstFrame")
            }
        })
        player!!.addListener(object : PlayerEventListener() {
            override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
                super.onPlayerStateChanged(playWhenReady, playbackState)
                when (playbackState) {
                    Player.STATE_READY -> Log.d("appLog", "STATE_READY")
                    Player.STATE_BUFFERING -> Log.d("appLog", "STATE_BUFFERING")
                    Player.STATE_IDLE -> Log.d("appLog", "STATE_IDLE")
                    Player.STATE_ENDED -> Log.d("appLog", "STATE_ENDED")
                }
            }
        })
        player!!.volume = 0f
        player!!.playWhenReady = true
        player!!.repeatMode = Player.REPEAT_MODE_ALL
        player!!.playVideoFromUrl(this@MainActivity, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv")
    }

    override fun onStop() {
        super.onStop()
        playerView.player = null
        player!!.release()
        player = null
    }


    abstract class PlayerEventListener : Player.EventListener {
        override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {}
        override fun onSeekProcessed() {}
        override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {}
        override fun onPlayerError(error: ExoPlaybackException?) {}
        override fun onLoadingChanged(isLoading: Boolean) {}
        override fun onPositionDiscontinuity(reason: Int) {}
        override fun onRepeatModeChanged(repeatMode: Int) {}
        override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
        override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {}
        override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {}
    }

    companion object {
        @JvmStatic
        fun getUserAgent(context: Context): String {
            val packageManager = context.packageManager
            val info = packageManager.getPackageInfo(context.packageName, 0)
            val appName = info.applicationInfo.loadLabel(packageManager).toString()
            return Util.getUserAgent(context, appName)
        }
    }

    fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri) {
        val dataSourceFactory = DefaultDataSourceFactory(context, MainActivity.getUserAgent(context))
        val mediaSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(uri)
        prepare(mediaSource)
    }


    fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String) = playVideoFromUri(context, Uri.parse(url))

    fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file))
}

What I've tried

I've tried reading on the docs, and got those links (by asking about it here ) :

https://medium.com/google-exoplayer/downloading-streams-6d259eec7f95 https://medium.com/google-exoplayer/downloading-adaptive-streams-37191f9776e

So sadly, currently the only solution I can come up with, is to download the file on another thread, which will cause the device to have 2 connections to it, thus using twice the bandwidth.

The questions

  1. How can I use ExoPlayer to play a video file, while also downloading it to some filepath ?
  2. Is there a way to enable a caching mechanism (which uses the disk) on ExoPlayer to be activated for the exact same purpose?

Note: To make it clear. I do not want to download the file and only then play it.


EDIT: I've found a way to get&use the file from the API's cache (wrote about it here), but it appears that this is considered as unsafe (written here).

So, given the simple cache mechanism that the API of ExoPlayer supports, my current questions are:

  1. If a file was cached, how can I use it in a safe manner?
  2. If a file was partially cached (meaning we've downloaded a part of it), how can I continue preparing it (without actually playing it or waiting for the whole playback to finish) till I can use it (in a safe manner of course) ?

I've made a Github repository for this here. You can try it out.

like image 706
android developer Avatar asked Dec 09 '18 12:12

android developer


People also ask

How do I play videos while downloading?

Just trace the file ending with . crdownload -> right click on the file -> in the "opens with" change button, select google chrome and then apply. now double click the file which has chrome logio then the file plays in chrome simultaneously while downloading.

Can we play YouTube video in ExoPlayer Android?

@MichaelStoddart Yes. It is a possibility. I read somewhere that using methods other than what was provided can be a violation. It applies to all Google service and APIs.

Does Netflix use ExoPlayer?

Over 140,000 applications in Google Play Store are using ExoPlayer for play media in these application such as Vevo, Twitter, BBC iPlayer, Netflix, Spotify, Facebook, Whatsapp, Twitch, and more applications include Fungjai.


1 Answers

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))
        }
    }
}
like image 184
Cheticamp Avatar answered Sep 23 '22 22:09

Cheticamp