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.
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))
}
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.
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:
I've made a Github repository for this here. You can try it out.
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.
@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.
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.
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:
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))
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With