I am trying to pre-cache/pre-buffer HLS videos to my app. I used CacheWriter to cache the (.mp4) file, But it was not able to cache segments of HLS video. Basically, I have only URL of the master playlist file which has media playlists of different qualities, and that each media playlist has segments (.ts).
So, I have to cache the master playlist and any one media playlist and then some segments and play the cached media to Exoplayer. how can I cache these?
I also visited https://github.com/google/ExoPlayer/issues/9337 But this does not have any example to do so.
This is how I cached .mp4 by CacheWriter
CacheWriter cacheWriter = new CacheWriter( mCacheDataSource,
dataSpec,
null,
progressListener);
cacheWriter.cache();
I am answering my own question for further users struggling on it.
We can pre-cache pre-cache HLS adaptive stream in ExoPlayer By using HlsDownloader provided by Exoplayer.
Add this Kotlin class to your project ExoPlayerModule.kt.
//SitaRam
package com.example.youtpackagename
import android.content.Context
import android.util.Log
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.source.hls.offline.HlsDownloader
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.upstream.FileDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.concurrent.CancellationException
//bytes to be downloaded
private const val PRE_CACHE_AMOUNT = 2 * 1048576L
class ExoPlayerModule(context: Context) {
private var cronetDataSourceFactory = DefaultHttpDataSource.Factory()
//StaticMember is class which contains cookie in my case, you can skip cookies and use DefaultHttpDataSource.Factory().
/*val Cookie = mapOf("Cookie" to StaticMember.getCookie())
private var cronetDataSourceFactory = if (StaticMember.getCookie() != null) {
DefaultHttpDataSource.Factory().setDefaultRequestProperties(Cookie)
}else {
DefaultHttpDataSource.Factory()
}*/
private val cacheReadDataSourceFactory = FileDataSource.Factory()
private var cache = simpleCache.SimpleCache(context)
private var cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
// .setCacheWriteDataSinkFactory(cacheSink)
.setCacheReadDataSourceFactory(cacheReadDataSourceFactory)
.setUpstreamDataSourceFactory(cronetDataSourceFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
fun isUriCached(uri: String, position: Long = 0): Boolean {
return cache.isCached(uri, position, PRE_CACHE_AMOUNT)
}
//updating cookies (if you are using cookies).
/* fun updateDataSourceFactory(){
val Cookie = mapOf("Cookie" to StaticMember.getCookie())
cronetDataSourceFactory = if (StaticMember.getCookie() != null) {
DefaultHttpDataSource.Factory().setDefaultRequestProperties(Cookie)
}else {
DefaultHttpDataSource.Factory()
}
cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(cache)
// .setCacheWriteDataSinkFactory(cacheSink)
.setCacheReadDataSourceFactory(cacheReadDataSourceFactory)
.setUpstreamDataSourceFactory(cronetDataSourceFactory)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
}*/
// TODO add the same for mp4. Also they might be a much better option, since they only have
// single track, so no matter what connection you have - loading can't happen twice
fun getHlsMediaSource(mediaItem: MediaItem): HlsMediaSource {
return HlsMediaSource.Factory(cacheDataSourceFactory)
.setAllowChunklessPreparation(true)
.createMediaSource(mediaItem)
}
fun releaseCache() = cache.release()
suspend fun preCacheUri(mediaItem: MediaItem) {
val downloader = HlsDownloader(mediaItem, cacheDataSourceFactory)
withContext(Dispatchers.IO) {
try {
downloader.download { _, bytesDownloaded, _ ->
if (MainActivity.nextUrl==mediaItem){
// Log.e("bytesCaching", "while: same $mediaItem same")
}else {
// Log.e("bytesCaching", "while: $mediaItem")
downloader.cancel()
}
if (bytesDownloaded >= PRE_CACHE_AMOUNT) {
// log("video precached at $percent%")
downloader.cancel()
}
}
} catch (e: Exception) {
if (e !is CancellationException) log("precache exception $e")
}
}
}
private fun log(s: String) {
TODO("Not yet implemented")
}
}
Initializing ExoPlayerModule
ExoPlayerModule PlayerModuleO = new ExoPlayerModule(MainActivity.this);
For Pre-Loading.
String previousUrl = "";
public void preLoad(String url) {
if (previousUrl.equals(url)) {
return;
}
previousUrl = url;
MediaItem mediaItem =MediaItem.fromUri(Uri.parse(url));
PlayerModuleO.preCacheUri(mediaItem, new Continuation<>() {
#NonNull
#Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
#Override
public void resumeWith(#NonNull Object o) {
}
});
}
Playing cached or non-cached media.
MediaItem mediaItem = MediaItem.fromUri(Uri.parse(url));
exoPlayer.setMediaSource(PlayerModuleO.getHlsMediaSource(mediaItem));
exoPlayer.prepare();
exoPlayer.play();
Releasing cache
PlayerModuleO.releaseCache();
If you are having any problems then feel free to ask.
Related
I want to save images taken from my app directly to a ssd drive (removable storage) plugged in my device.
The issue I have now, is that with Android 11, I didn't manage to get the path of this storage, and so I can't write the files...
I tried use Storage Access Framework to ask the user to specify the path directly for each images but I can't use this solution as I need to write 30 images per seconds and it kept asking the user select an action on the screen.
This application is only for internal use, so I can grant all the permission without any Google deployment politics issues.
Can anybody help me, i'm so desperate...
So here's my code, I can write on a folder the user choose with SAF. Still have speed issue using DocumentFile.createFile function.
package com.example.ssdwriter
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.*
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.documentfile.provider.DocumentFile
class MainActivity : AppCompatActivity() {
private val TAG = "SSDActivity"
private val CONTENT = ByteArray(2 * 1024 * 1024)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
grantDirectoryAccess()
}
private fun grantDirectoryAccess() {
val treeUri = contentResolver.persistedUriPermissions
if (treeUri.size > 0) {
Log.e(TAG, treeUri.size.toString())
startWriting(treeUri[0].uri)
} else {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
var resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data: Intent? = result.data
result.data?.data?.let {
contentResolver.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
startWriting(result.data?.data!!)
}
}
resultLauncher.launch(intent)
}
}
private fun startWriting(uri: Uri) {
var handlerThread = HandlerThread("writer")
handlerThread.start()
var counter = 0
val handler = Handler(handlerThread.looper)
val runnableCode: Runnable = object : Runnable {
override fun run() {
Log.e(TAG, "Writing File $counter")
createFile(uri, counter++)
Log.e(TAG, "File $counter written ")
if(counter <= 150){
handler.postDelayed(this, 33)
}
}
}
handler.post(runnableCode)
}
private fun createFile(treeUri: Uri, counter: Int) {
val dir = DocumentFile.fromTreeUri(this, treeUri)
val file = dir!!.createFile("*/bmp", "Test$counter.bmp")
if (file != null) {
var outputStream = contentResolver.openOutputStream(file.uri)
if (outputStream != null) {
outputStream.write(CONTENT)
outputStream.flush()
outputStream.close()
}
}
}
}
If anyone got some clues to make this faster, it would be great !
I recently asked a question on how to get the HTML code from a Google API Script that fed me an IP Address, and received an answer that told me to use this Utility Class. (I am coding this in Kotlin)
package com.example.myapplication
import androidx.appcompat.app.AppCompatActivity
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.MalformedURLException
import java.net.URL
object ContentScrapper {
fun getHTMLData(activity: AppCompatActivity,url: String, scrapListener: ScrapListener) {
Thread(Runnable {
val google: URL?
val `in`: BufferedReader?
var input: String?
val stringBuffer = StringBuffer()
try {
google = URL(url)
`in` = BufferedReader(InputStreamReader(google.openStream()))
while (true) {
if (`in`.readLine().also { input = it } == null)
break
stringBuffer.append(input)
}
`in`.close()
activity.runOnUiThread {
scrapListener.onResponse(stringBuffer.toString())
}
} catch (e: MalformedURLException) {
e.printStackTrace()
activity.runOnUiThread {
scrapListener.onResponse(null)
}
}
}).start()
}
interface ScrapListener {
fun onResponse(html: String?)
}
}
And called it using this
ContentScrapper.getHTMLData(this, url, object : ContentScrapper.ScrapListener{
override fun onResponse(html: String?) {
if(html != null) {
editTexttest.setText(html)
} else {
editTexttest.setText("Not Found")
}
}
})
However, I was unable to find the string that was displayed on my screen. Below is the link that I am currently using to get the string. The HTML code that is returned to me doesn't seem to contain the string that the url below provides. Is there anything wrong with how I called it? Any help is appreciated. Thanks.
https://script.google.com/macros/s/AKfycbyjRIRl2ca_pnfz8XgccjDlaRPUNz6KY_WcyPZAROsy9EZkD35F/exec?command=GetLock1IPAddress
I'm trying to play a video with the use of ExoPlayer API and by using the exoplayer library of version : 'com.google.android.exoplayer:exoplayer:2.8.1'. I want to play a video called video.mp4 which is in a folder called folder1 and this folder in inside the folder assets in the res (res/assets/folder1/video.mp4). I cannot get my code to play the video. Please help me.
My MainActivity.java:
package com.example.amandeep.example2;
import android.content.pm.ActivityInfo;
import android.net.Uri;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.util.ArrayList;
public class MainActivity extends AppCompatActivity
{
SimpleExoPlayer video_player;
PlayerView player_screen;
DefaultTrackSelector track_selector;
DefaultBandwidthMeter band_width_meter = new DefaultBandwidthMeter();
MediaSource mediaSource;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
player_screen = findViewById (R.id.player_screen);
player_screen.requestFocus();
TrackSelection.Factory video_track_selection_factory = new AdaptiveTrackSelection.Factory(band_width_meter);
track_selector = new DefaultTrackSelector(video_track_selection_factory);
video_player = ExoPlayerFactory.newSimpleInstance(this, track_selector);
player_screen.setPlayer(video_player);
video_player.setPlayWhenReady(true);
DataSource.Factory data_source_factory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, "Application Name"), new DefaultBandwidthMeter());
Uri url = Uri.parse("file:///android_asset/folder/video.mp4");
mediaSource = new ExtractorMediaSource.Factory(data_source_factory).createMediaSource(url);
video_player.prepare(mediaSource);
}
}
My activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
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/player_screen"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:use_controller="true" />
</android.support.constraint.ConstraintLayout>
Note: I want to use the Uri.parse method.
IF SOMEONE CANNOT UNDERSTAND MY QUESTION PROPERLY, PLEASE COMMENT BELOW.
IF URI.PARSE METHOD CANNOT USE FILES FROM ASSETS THEN PLEASE TELL ME
ExoPlayer 2.12 introduces the MediaItem class so you can do:
val firstVideoUri = Uri.parse("asset:///localfile.mp4")
val firstItem = MediaItem.fromUri(firstVideoUri)
player.addMediaItem(firstItem)
Note that the URI should start with asset:/// not assets:///
This is my code to play audio file, hope it helps you.
private void prepareExoPlayerFromAssetResourceFile(int current_file) {
exoPlayer = ExoPlayerFactory.newSimpleInstance(this, new DefaultTrackSelector((TrackSelection.Factory) null), new DefaultLoadControl());
exoPlayer.addListener(eventListener);
//DataSpec dataSpec = new DataSpec(uri);
//DataSpec dataSpec = new DataSpec(Uri.parse("asset:///001.mp3"));
DataSpec dataSpec = new DataSpec(Uri.parse("asset:///" + current_file +".mp3"));
final AssetDataSource assetDataSource = new AssetDataSource(this);
try {
assetDataSource.open(dataSpec);
} catch (AssetDataSource.AssetDataSourceException e) {
e.printStackTrace();
}
DataSource.Factory factory = new DataSource.Factory() {
#Override
public DataSource createDataSource() {
//return rawResourceDataSource;
return assetDataSource;
}
};
MediaSource audioSource = new ExtractorMediaSource(assetDataSource.getUri(),
factory, new DefaultExtractorsFactory(), null, null);
exoPlayer.prepare(audioSource);
initMediaControls();
}
Try a uri of this format:
file:///android_asset/
For your example that would need to be:
Uri url = Uri.parse("file:///android_asset/folder1/video.mp4");
I'm using similar code for playing video from my assets folder with ExoPlayer and it works good.
But I noticed you mentioned diferent folders in description and code. You are saying the folder in assets is named folder1 but in code you have folder.
So make sure you have correct path to video.
I write this answer because some methods on old answers deprecated :
fun prepareExoPlayerFromAssetResource(
uri: String = "asset://example.mp3")
{
val dataSourceFactory = DataSource.Factory {AssetDataSource(context)}
val mediaSource = ProgressiveMediaSource
.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(uri)))
exoPlayer.addMediaSource(audioSource)
exoPlayer.prepare()
exoPlayer.play()
}
I hope this is useful for someone :)
This may be useful:
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.upstream.*
import com.google.android.exoplayer2.upstream.DataSource.Factory
import com.google.android.exoplayer2.util.Util
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : AppCompatActivity(), Player.EventListener {
private var player: SimpleExoPlayer? = null
private var playWhenReady = true
private var currentWindow = 0
private var playbackPosition: Long = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
public override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
initializePlayer("assets:///your_file.your_extension") //"assets:///pillwheel_pills.mp4"
}
}
public override fun onResume() {
super.onResume()
hideSystemUi()
if (Util.SDK_INT <= 23 || player == null) {
initializePlayer("assets:///your_file.your_extension") //"assets:///pillwheel_pills.mp4"
}
}
public override fun onPause() {
super.onPause()
if (Util.SDK_INT <= 23) {
releasePlayer()
}
}
public override fun onStop() {
super.onStop()
if (Util.SDK_INT > 23) {
releasePlayer()
}
}
private fun initializePlayer(path: String) {
if (player == null) {
val trackSelector = DefaultTrackSelector(this)
trackSelector.setParameters(
trackSelector.buildUponParameters().setMaxVideoSizeSd())
player = SimpleExoPlayer.Builder(this)
.setTrackSelector(trackSelector)
.build()
}
video_view?.player = player
video_view?.requestFocus()
val dataSourceFactory = Factory { AssetDataSource(this#MainActivity) }
val videoSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(Uri.parse(path))
player?.playWhenReady = playWhenReady
player?.seekTo(currentWindow, playbackPosition)
player?.addListener(this)
player?.prepare(videoSource)
}
private fun releasePlayer() {
if (player != null) {
playbackPosition = player?.currentPosition!!
currentWindow = player?.currentWindowIndex!!
playWhenReady = player?.playWhenReady!!
player?.removeListener(this)
player?.release()
player = null
}
}
/**
* set fullscreen
*/
#SuppressLint("InlinedApi")
private fun hideSystemUi() {
video_view?.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
}
override fun onPlayerError(error: ExoPlaybackException) {
super.onPlayerError(error)
//handle the error
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
val stateString: String = when (playbackState) {
ExoPlayer.STATE_IDLE -> "ExoPlayer.STATE_IDLE -"
ExoPlayer.STATE_BUFFERING -> "ExoPlayer.STATE_BUFFERING -"
ExoPlayer.STATE_READY -> "ExoPlayer.STATE_READY -"
ExoPlayer.STATE_ENDED -> "ExoPlayer.STATE_ENDED -"
else -> "UNKNOWN_STATE -"
}
Log.d(TAG, "changed state to " + stateString
+ " playWhenReady: " + playWhenReady)
}
companion object {
private val TAG = MainActivity::class.java.name
}
}
You can find the full source code on GitHub
I have implemented the ExoPlayer in my application using the example from the Codelab : https://codelabs.developers.google.com/codelabs/exoplayer-intro/#3, algo with the example from https://medium.com/google-exoplayer/playing-ads-with-exoplayer-and-ima-868dfd767ea, the only difference is that I use AdsMediaSource instead of the deprecated ImaAdsMediaSource.
My Implementation is this:
class HostVideoFullFragment : Fragment(), AdsMediaSource.MediaSourceFactory {
override fun getSupportedTypes() = intArrayOf(C.TYPE_DASH, C.TYPE_HLS, C.TYPE_OTHER)
override fun createMediaSource(uri: Uri?, handler: Handler?, listener: MediaSourceEventListener?): MediaSource {
#C.ContentType val type = Util.inferContentType(uri)
return when (type) {
C.TYPE_DASH -> {
DashMediaSource.Factory(
DefaultDashChunkSource.Factory(mediaDataSourceFactory),
manifestDataSourceFactory)
.createMediaSource(uri, handler, listener)
}
C.TYPE_HLS -> {
HlsMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener)
}
C.TYPE_OTHER -> {
ExtractorMediaSource.Factory(mediaDataSourceFactory)
.createMediaSource(uri, handler, listener)
}
else -> throw IllegalStateException("Unsupported type for createMediaSource: $type")
}
}
private var player: SimpleExoPlayer? = null
private lateinit var playerView: SimpleExoPlayerView
private lateinit var binding: FragmentHostVideoFullBinding
private var playbackPosition: Long = 0
private var currentWindow: Int = 0
private var playWhenReady = true
private var inErrorState: Boolean = false
private lateinit var adsLoader: ImaAdsLoader
private lateinit var manifestDataSourceFactory: DataSource.Factory
private lateinit var mediaDataSourceFactory: DataSource.Factory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//Initialize the adsLoader
adsLoader = ImaAdsLoader(activity as Context, Uri.parse("https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/ad_rule_samples&ciu_szs=300x250&ad_rule=1&impl=s&gdfp_req=1&env=vp&output=vmap&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ar%3Dpremidpost&cmsid=496&vid=short_onecue&correlator="))
manifestDataSourceFactory = DefaultDataSourceFactory(
context, Util.getUserAgent(context, "BUO-APP"))//TODO change the applicationName with the right application name
//
mediaDataSourceFactory = DefaultDataSourceFactory(
context,
Util.getUserAgent(context, "BUO-APP"),//TODO change the applicationName with the right application name
DefaultBandwidthMeter())
}
private fun initializePlayer() {
/*
* Since the player can change from null (when we release resources) to not null we have to check if it's null.
* If it is then reset again
* */
if (player == null) {
//Initialize the Exo Player
player = ExoPlayerFactory.newSimpleInstance(DefaultRenderersFactory(activity as Context),
DefaultTrackSelector())
}
val uri = Uri.parse(videoURl)
val mediaSourceWithAds = buildMediaSourceWithAds(uri)
//Bind the view from the xml to the SimpleExoPlayer instance
playerView.player = player
//Add the listener that listens for errors
player?.addListener(PlayerEventListener())
player?.seekTo(currentWindow, playbackPosition)
player?.prepare(mediaSourceWithAds, true, false)
//In case we could not set the exo player
player?.playWhenReady = playWhenReady
//We got here without an error, therefore set the inErrorState as false
inErrorState = false
//Re update the retry button since, this method could have come from a retry click
updateRetryButton()
}
private inner class PlayerEventListener : Player.DefaultEventListener() {
fun updateResumePosition() {
player?.let {
currentWindow = player!!.currentWindowIndex
playbackPosition = Math.max(0, player!!.contentPosition)
}
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
//The player state has ended
//TODO check if there is going to be a UI change here
// if (playbackState == Player.STATE_ENDED) {
// showControls()
// }
// updateButtonVisibilities()
}
override fun onPositionDiscontinuity(#Player.DiscontinuityReason reason: Int) {
if (inErrorState) {
// This will only occur if the user has performed a seek whilst in the error state. Update
// the resume position so that if the user then retries, playback will resume from the
// position to which they seek.
updateResumePosition()
}
}
override fun onPlayerError(e: ExoPlaybackException?) {
var errorString: String? = null
//Check what was the error so that we can show the user what was the correspond problem
if (e?.type == ExoPlaybackException.TYPE_RENDERER) {
val cause = e.rendererException
if (cause is MediaCodecRenderer.DecoderInitializationException) {
// Special case for decoder initialization failures.
errorString = if (cause.decoderName == null) {
when {
cause.cause is MediaCodecUtil.DecoderQueryException -> getString(R.string.error_querying_decoders)
cause.secureDecoderRequired -> getString(R.string.error_no_secure_decoder,
cause.mimeType)
else -> getString(R.string.error_no_decoder,
cause.mimeType)
}
} else {
getString(R.string.error_instantiating_decoder,
cause.decoderName)
}
}
}
if (errorString != null) {
//Show the toast with the proper error
Toast.makeText(activity as Context, errorString, Toast.LENGTH_LONG).show()
}
inErrorState = true
if (isBehindLiveWindow(e)) {
clearResumePosition()
initializePlayer()
} else {
updateResumePosition()
updateRetryButton()
}
}
}
private fun clearResumePosition() {
//Clear the current resume position, since there was an error
currentWindow = C.INDEX_UNSET
playbackPosition = C.TIME_UNSET
}
/*
* This is for the multi window support
* */
private fun isBehindLiveWindow(e: ExoPlaybackException?): Boolean {
if (e?.type != ExoPlaybackException.TYPE_SOURCE) {
return false
}
var cause: Throwable? = e.sourceException
while (cause != null) {
if (cause is BehindLiveWindowException) {
return true
}
cause = cause.cause
}
return false
}
private fun buildMediaSourceWithAds(uri: Uri): MediaSource {
/*
* This content media source is the video itself without the ads
* */
val contentMediaSource = ExtractorMediaSource.Factory(
DefaultHttpDataSourceFactory("BUO-APP")).createMediaSource(uri) //TODO change the user agent
/*
* The method constructs and returns a ExtractorMediaSource for the given uri.
* We simply use a new DefaultHttpDataSourceFactory which only needs a user agent string.
* By default the factory will use a DefaultExtractorFactory for the media source.
* This supports almost all non-adaptive audio and video formats supported on Android. It will recognize our mp3 file and play it nicely.
* */
return AdsMediaSource(
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
playerView.overlayFrameLayout,
/* eventListener= */ null, null)
}
override fun onStart() {
super.onStart()
if (Util.SDK_INT > 23) {
initializePlayer()
}
}
override fun onResume() {
super.onResume()
hideSystemUi()
/*
* Starting with API level 24 Android supports multiple windows.
* As our app can be visible but not active in split window mode, we need to initialize the player in onStart.
* Before API level 24 we wait as long as possible until we grab resources, so we wait until onResume before initializing the player.
* */
if ((Util.SDK_INT <= 23 || player == null)) {
initializePlayer()
}
}
}
The ad never shows and if it shows it shows a rendering error E/ExoPlayerImplInternal: Renderer error. which never allows the video to show. I've run the examples from the IMA ads https://developers.google.com/interactive-media-ads/docs/sdks/android/ example code and it doesn't work neither. Does anyone have implemented the Exo Player succesfully with the latest ExoPlayer library version?
Please Help. Thanks!
When on an emulator, be sure to enable gpu rendering on the virtual device
The problem is that the emulator can not render videos. Therefore it wasn't showing the ads or the video. Run the app on a phone and it will work
I am not a fan of polling for information and suspect there is a better way of achieveing what I want.
I am playing an internet radio stream with Android's MediaPlayer. I can find out which tune is playing and by which artist by requesting the 7.html file at the server's address.
My questions are:
Is there a way to receive a notification when a new song begins
to play?
Must I poll the 7.html to find out what is now playing?
If I do have to poll, is there any way in which I can determine
the duration of the current song so I can poll only when a new song
starts?
I guess if I had a low-level stream processing function of my own, I could tell when the song changes because I would receive the meta-data, but I'm not sure how to do that with the Android MediaPlayer class.
Haha, seven years after commenting I finally had to implement this :-D I want a tumbleweed badge for this ;-)
Not to my knowledge
Yes
Not to my knowledge, but polling timers between 30-60 seconds should be fine. At the beginning I wanted to reduce network traffic for users, but this is irrelevant if you are streaming radio at the same time :-D
And here my quick and dirty solution, just in case someone needs it. There are some custom classes in the example, but you ll get the point
import androidx.core.text.HtmlCompat
import de.jahpress.android.main.L
import de.jahpress.android.main.MAX_REQUEST_FOR_SHOUTCAST_TRACK_INFO
import de.jahpress.android.service.Data
import de.jahpress.android.service.radio.model.BaseStation
import okhttp3.OkHttpClient
import okhttp3.Request
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
class ShoutCastTrackInfoManager {
private val timeOut = 5L
private val pollingIntervalMs = 60_000L
private var updateTimer: Timer? = null
private var trackInfoThread: Thread? = null
private var invalidTrackInfoCounter = 0
//will ask track info only one time (select station in my use case)
fun updateTrackInfoFor(station: BaseStation, resultCallback: (info: String?) -> Unit) {
L.d("TrackInfo: Get title info for ${station.getStationName()}")
invalidTrackInfoCounter = 0
stopTrackInfoPolling()
requestTrackInfoFromShoutcast(station, resultCallback)
}
//will start track info polling (if station is playing)
fun startTrackInfoPolling(station: BaseStation) {
L.d("TrackInfo: Get title info for ${station.getStationName()}")
stopTrackInfoPolling()
updateTimer = Timer()
updateTimer?.schedule(object : TimerTask() {
override fun run() {
requestTrackInfoFromShoutcast(station, null)
}
}, 0, pollingIntervalMs)
}
fun stopTrackInfoPolling() {
trackInfoThread?.let {
L.d("TrackInfo: Stopping current title update for stream")
it.interrupt()
}
updateTimer?.cancel()
}
private fun requestTrackInfoFromShoutcast(
station: BaseStation,
resultCallback: ((info: String?) -> Unit)?
) {
if (invalidTrackInfoCounter >= MAX_REQUEST_FOR_SHOUTCAST_TRACK_INFO) {
L.d("TrackInfo: $MAX_REQUEST_FOR_SHOUTCAST_TRACK_INFO invalid stream titles. Sto...")
invalidTrackInfoCounter = 0
stopTrackInfoPolling()
Data.currentTitleInfo = null //reset track info
return
}
trackInfoThread = thread {
try {
var trackInfo: String? = null
get7HtmlFromStream(station)?.let {
L.d("TrackInfo: Request track info at $it")
val request = Request.Builder().url(it).build()
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(timeOut, TimeUnit.SECONDS)
.writeTimeout(timeOut, TimeUnit.SECONDS)
.readTimeout(timeOut, TimeUnit.SECONDS)
.build()
val response = okHttpClient.newCall(request).execute()
if (response.isSuccessful) {
val result = response.body?.string()
trackInfo = extractTrackInfoFrom7Html(result)
if (trackInfo != null) {
Data.currentTitleInfo = trackInfo
}
}
response.close()
}
resultCallback?.invoke(trackInfo)
} catch (e: Exception) {
L.e(e)
resultCallback?.invoke(null)
stopTrackInfoPolling()
}
}
}
/**
* Will create Shoutcast 7.html which is located at stream url.
*
* For example: http://66.55.145.43:7473/stream
* 7.html at http://66.55.145.43:7473/7.html
*/
private fun get7HtmlFromStream(station: BaseStation): String? {
val baseStreamUrl = station.getStreamUrl()
L.w("Base url -> $baseStreamUrl")
if (baseStreamUrl == null) return null
val numberSlash = baseStreamUrl.count { c -> c == '/' }
if (numberSlash <= 2) {
return "$baseStreamUrl/7.html"
}
val startOfPath = station.getStreamUrl().indexOf("/", 8)
val streamUrl = station.getStreamUrl().subSequence(0, startOfPath)
return "$streamUrl/7.html"
}
/**
* Will convert webpage to trackinfo. Therefore
* 1. Remove all html-tags
* 2. Get <body> content of webpage
* 3. Extract and return trackinfo
*
* Trackinfo format is always like
* "632,1,1943,2000,439,128,Various Artists - Dance to Dancehall"
* so method will return everything after sixth "," comma character.
*
* Important:
* - Shoutcast might return invalid html
* - Site will return 404 error strings
* - might be empty
*/
private fun extractTrackInfoFrom7Html(html: String?): String? {
L.i("Extract track info from -> $html")
if (html == null) return null
val content = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
val array = content.split(",")
return if (array.size < 7) {
null
} else {
var combinedTrackInfo = ""
for (index in 6 until array.size) {
combinedTrackInfo += "${array[index]} "
}
if (combinedTrackInfo.trim().isEmpty()) {
return null
}
return combinedTrackInfo
}
}
}