I am only able to broadcast audio alone using mic and speaker, and if I use setExternalAudioSource method, then the broadcast encounter with some heavy unwanted noise. I just want to broadcast the raw audio data alone without using mic, speaker and unwanted noise.
private val PERMISSION_REQ_ID_RECORD_AUDIO = 22
private var mRtcEngine: RtcEngine? = null// Tutorial Step 1
private val mRtcEventHandler = object : IRtcEngineEventHandler() { // Tutorial Step 1
override fun onUserOffline(uid: Int, reason: Int) { // Tutorial Step 4
//runOnUiThread { onRemoteUserLeft(uid, reason) }
}
override fun onUserMuteAudio(uid: Int, muted: Boolean) { // Tutorial Step 6
// runOnUiThread { onRemoteUserVoiceMuted(uid, muted) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO, PERMISSION_REQ_ID_RECORD_AUDIO)) {
createRtcChannel()
}
}
fun checkSelfPermission(permission: String, requestCode: Int): Boolean {
if (ContextCompat.checkSelfPermission(this,
permission) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
arrayOf(permission),
requestCode)
return false
}
return true
}
private fun createRtcChannel() {
initializeAgoraEngine() // Tutorial Step 1
joinChannel()
}
private fun initializeAgoraEngine() {
try {
mRtcEngine = RtcEngine.create(this, getString(R.string.agora_app_id), mRtcEventHandler)
//set the channel as live broadcast mode
mRtcEngine?.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
mRtcEngine?.setClientRole(Constants.CLIENT_ROLE_BROADCASTER)
} catch (e: Exception) {
}
}
private fun joinChannel() {
mRtcEngine?.joinChannel(null, "voiceDemoChannel1", "Extra Optional Data", 0) // if you do not specify the uid, we will generate the uid for you
val payload = IOUtils.toByteArray(assets.openFd("ringtone.mp3").createInputStream())
mRtcEngine?.setExternalAudioSource(
true,
8000,
1 );
mRtcEngine?.pushExternalAudioFrame(
payload,
1000
)
}
Is this possible using agora or is there any alternative to it?
Reasons can cause the noise:
Your source audio PCM samples are noisy by themselves
Engine bugs
The sample rate you set is wrong
For the first one, you can check your PCM samples directly. For the 2nd, as there are already many people using, it's rare to be true. So I woulld suguest you to check the sample rate if you are sure your source PCM samples are good.
Also, you can set up the APM option before you join channel to enable audio enhancement for external source, by
setParameters("{\"che.audio.override_apm\":true}")
Related
Description of the problem:
When the app is connected through Bluetooth headsets, I'm starting the audioManager.startBluetoothSco(). You can check the code below. The connection works well, but I have a problem with the crackling sounds that are played by MediaPlayer. Whenever an action sound is played, the quality is bad or a crackling sound happens. (eg. R.raw.record_start, R.raw.success_action, R.raw.failure_action)
Devices that I used to test:
OpenComm by AfterShokx and Jabra Evolve 65
Samsung A52, Samsung A50, Pixel 3, OnePlus 6 Pro
What I've tried so far?
I tried to change audioManager.mode = AudioManager.MODE_IN_COMMUNICATION to AudioManager.MODE_IN_CALL or AudioManager.MODE_NORMAL.
Changing to MODE_IN_CALL doesn't change the audioManager.mode. The value stays as 0 which is MODE_NORMAL.
Changing to MODE_NORMAL only works on Samsung A52. No crackling sounds everything works perfectly. This is what I want to achieve,
BUT all other phones switch to a different stream or something else; that's why I can't HEAR any media sounds.
Let's say I use Pixel 3 and connected with Jabra Evolve 65 in MODE_NORMAL. I can't hear any kind of media sounds anymore including other apps like Youtube, Spotify, and system sounds, but it works in MODE_IN_COMMUNICATION with a crackling sound.
I tried to change MediaPlayer's AudioAttributes but still bad quality.
val actionSound = if (triggerErrorEarcon) R.raw.failure_action else if (longRecordingBreak) R.raw.success_action else R.raw.record_start
val md = MediaPlayer.create(context, actionSound)
val streamType = if (audioDeviceManager.headsetConnected) AudioManager.STREAM_VOICE_CALL else AudioManager.STREAM_MUSIC
md.setAudioAttributes(
AudioAttributes.Builder()
.setLegacyStreamType(streamType)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
md.play()
Possible Solutions?
I don't know how but fixing audioManager.mode or some other properties of audioManager.
Keep using MODE_IN_COMMUNICATION but a fix on MediaPlayer properties to disable bad quality of sound on Bluetooth device.
I can't think of anything else from this point. I hope you could help me with this problem, thanks in advance.
AudioDeviceManager.kt:
class AudioDeviceManager(val context: Context) {
internal val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
internal val headsetConnectedSubject = BehaviorSubject.createDefault(false)
internal val headsetConnected: Boolean get() = headsetConnectedSubject.value ?: false
private val intentFilter = IntentFilter().apply { addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED) }
private val audioDeviceCallback = object: AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
super.onAudioDevicesAdded(addedDevices)
updateBluetoothHeadsetState()
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
super.onAudioDevicesRemoved(removedDevices)
updateBluetoothHeadsetState()
}
}
private val scoReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1) == AudioManager.SCO_AUDIO_STATE_CONNECTED) {
// SCO now connected
audioManager.mode = AudioManager.MODE_IN_COMMUNICATION
audioManager.isSpeakerphoneOn = false
audioManager.isBluetoothScoOn = true
}
}
}
fun start() {
if (headsetConnected.not())
audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)
}
fun stop(unregisterAudioDeviceCallback: Boolean = false) {
if (unregisterAudioDeviceCallback)
unregisterDeviceCallback()
audioManager.mode = AudioManager.MODE_NORMAL
audioManager.isSpeakerphoneOn = true
audioManager.isBluetoothScoOn = false
audioManager.stopBluetoothSco()
}
private fun unregisterDeviceCallback() = audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)
fun updateBluetoothHeadsetState() {
val headset = audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)?.firstOrNull { it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO }
val headsetConnected = headset != null
headsetConnectedSubject.onNext(headsetConnected)
if (headsetConnected) {
audioManager.startBluetoothSco()
context.registerReceiver(scoReceiver, intentFilter)
} else {
audioManager.mode = AudioManager.MODE_NORMAL
audioManager.isSpeakerphoneOn = true
audioManager.isBluetoothScoOn = false
audioManager.stopBluetoothSco()
}
}
}
Playing an action sound in the app:
MediaPlayer.create(context, R.raw.record_start)
.play()
.subscribe()
.addTo(disposable)
Extension function:
fun MediaPlayer.play(): Completable {
return Completable.create { emitter ->
var isCancelled = false
emitter.setCancellable {
isCancelled = true
}
setOnCompletionListener {
GlobalScope.launch {
// release the sound a bit later. Listener is triggering so fast!
delay(3000)
it.release()
}
if (isCancelled) { return#setOnCompletionListener }
emitter.onComplete()
}
start()
}
}
I'm trying to create an Android app that connects to the Doorbird device, I know the company's official app, but, I need more features that are tailored to my needs.
For someone that doesn't know what is Doorbird device, Doorbird is a smart intercom, a product of Doorbird company, the device can transmit audio and video from him to any consumer, like Android system, over HTTP and RTSP and he can get Audio stream and play it, for example, to record audio from Android device and transmit it to Doorbird. The audio is in format G711 u-law.
I was able to get the video and audio stream received from Doorbird and it works perfectly but I don't succeed to transmit the audio, in the u-law format of course, to Doorbird.
The error I get is
HTTP FAILED: java.net.ProtocolException: Unexpected status line:
I tried to transmit the same bytes I get from Doorbird back to Doorbird but still the same error.
Of course, I work according to the API that they published but there is not much information about an agreed protocol to transmit audio.
Offical Doorbird API
Is there an example of an Android project that integrates with Doorbird?
Can anyone help in trying to broadcast audio to Doorbird?
Which protocol should be?
Even someone who knows to transmit audio to Doorbird with any other tools and any system and not just Android OS, I'd appreciate it.
This is what I tried, I received the data from Doorbird (and as I said its works) and waiting 3 seconds, and transmit it with Retrofit Libray back to Doorbird.
const val AUDIO_PATH =
"http://192.168.1.187/bha-api/audio-receive.cgi?http-user=XXXXXX0001&http-password=XXXXXXXXXX"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//InputStream inputStream = getResources().openRawResource(R.raw.piano12);
val thread = Thread { this.playUrl() }
thread.start()
//val inStr = assets.open("doorbird_record")
}
private fun playUrl() {
val inStr = URL(AUDIO_PATH).openStream()
val buffer = ByteArray(1000)
var i = 0
//while (inStr.read(buffer).also { i = it } != -1) {
Handler(Looper.getMainLooper()).postDelayed({
//inStr.close()
inStr.read(buffer)
Log.d("DoorbirdLog", inStr.toString())
val part = MultipartBody.Part.createFormData(
"doorbirdStream", "doorbird", buffer.toRequestBody(
("audio/basic").toMediaType()
)
)
//val rb = file.asRequestBody(("audio/*").toMediaType())
val call = NetworkManager.instanceServiceApi.upload(part)
call.enqueue(object : Callback<ResponseBody> {
override fun onResponse(
call: Call<ResponseBody>,
response: Response<ResponseBody>
) {
val i = response.body()
Log.d("success", i.toString())
}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
Log.d("failed", t.message.toString())
}
})
}, 3000)
}
And the Retrofit instance:
#Multipart
#Headers( "Content-Type: audio/basic",
"Content-Length: 9999999",
"Connection: Keep-Alive",
"Cache-Control: no-cache")
#POST("audio-transmit.cgi?http-user=XXXXXX0001&http-password=XXXXXXXXXX")
fun upload(#Part part: MultipartBody.Part): Call<ResponseBody>
I'd appreciate your assistance
Eventually, I was able to find a solution, I'll briefly present here the solution for those who will encounter an attempt to integrate with Doorbird.
private const val FREQUENCY_SAMPLE_RATE_TRANSMIT = 8000
private const val RECORD_STATE_STOPPED = 0
override suspend fun recordAndTransmitAudio(audioTransmitUrl: String) =
withContext(Dispatchers.IO) {
val minBufferSize = AudioRecord.getMinBufferSize(
FREQUENCY_SAMPLE_RATE_TRANSMIT, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
mRecorder = AudioRecord(
MediaRecorder.AudioSource.VOICE_COMMUNICATION,
FREQUENCY_SAMPLE_RATE_TRANSMIT, AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT, minBufferSize
)
mRecorder?.let { enableAcousticEchoCanceler(it.audioSessionId) }
mRecorder?.startRecording()
val bufferShort = ShortArray(minBufferSize)
val buffer = ByteArray(minBufferSize)
val urlConnection = URL(audioTransmitUrl).openConnection() as HttpURLConnection
urlConnection.apply {
doOutput = true
setChunkedStreamingMode(minBufferSize)
}
val output = DataOutputStream(urlConnection.outputStream)
output.flush()
try {
mRecorder?.let { recorder ->
while (recorder.read(bufferShort, 0, bufferShort.size) != RECORD_STATE_STOPPED) {
G711UCodecManager.encode(bufferShort, minBufferSize, buffer, 0)
output.write(buffer)
}
}
}catch (e: Exception){
Log.d(TAG, e.message.toString())
}
output.close()
urlConnection.disconnect()
}
First, we will prepare the necessary parameters for recording and transmission
We get the minimum size of the buffer for recording
Define the object with which we will record
Activate the echo cancellation
And start recording
Open connection with the transmit URL
While loop as long as the recording has not stopped
Encode the data we recorded from PCM 16Bit format to G.711 μ-law format
And of course, after we finished the recording we cleaned up resources.
I am using Exoplayer to create my own music player. I am also adding the option to download the track but I have a problem when I am trying to download the track that I am playing. I add a notification to the download to check the progress of the download and it appears but it even doesn't start. What I think is that it might have some kind of problem with the buffering cache and the download since they are stored in the same folder.
To download the tracks I do the following:
override fun addDownloadTrack(track: Track) {
getIfTrackIsCached.run({ isCached ->
if (!isCached) {
val data = Util.toByteArray(track.title.byteInputStream())
val downloadRequest =
DownloadRequest(track.id, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(track.href), Collections.emptyList(), track.id, data)
DownloadService.sendAddDownload(context, ExoPlayerDownloadService::class.java, downloadRequest, false)
}
}, ::onError, GetIfTrackIsCached.Params(track.id))
}
This is the DownloadService:
class ExoPlayerDownloadService : DownloadService(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
Constants.CHANNEL_DOWNLOAD_ID,
R.string.exo_download_notification_channel_name) {
private val manager: DownloadManager by inject()
private val channelIsCached: ChannelPublisher<CachedMedia> by inject(PUBLISHER_IS_CACHED)
private val notificationHelper: DownloadNotificationHelper by inject()
private var nextNotificationId: Int = FOREGROUND_NOTIFICATION_ID
override fun onCreate() {
super.onCreate()
if (!manager.isInitialized) {
manager.maxParallelDownloads = MAX_PARALLEL_DOWNLOADS
}
}
override fun getDownloadManager(): DownloadManager = manager
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification {
var text = ""
var index = 1
downloads?.forEach { text += "${if (downloads.size > 1) "${index++} - " else ""}${Util.fromUtf8Bytes(it.request.data)}\n" }
return notificationHelper.buildProgressNotification(R.drawable.ic_stat_downloading, null, text, downloads)
}
override fun getScheduler(): Scheduler? = null
override fun onDownloadChanged(download: Download?) {
val notification = when (download?.state) {
Download.STATE_COMPLETED -> {
channelIsCached.publish(CachedMedia(download.request.id, true))
notificationHelper.buildDownloadCompletedNotification(R.drawable.ic_stat_download_complete, null, Util.fromUtf8Bytes(download.request.data))
}
Download.STATE_FAILED ->
notificationHelper.buildDownloadFailedNotification(R.drawable.ic_stat_download_failed, null, Util.fromUtf8Bytes(download.request.data))
else -> null
}
notification?.let { NotificationUtil.setNotification(this#ExoPlayerDownloadService, ++nextNotificationId, it) }
}
companion object {
private const val MAX_PARALLEL_DOWNLOADS = 3
private const val FOREGROUND_NOTIFICATION_ID = 2000
}
}
And to create the cache I use this:
SimpleCache(File(androidContext().cacheDir, CACHE_MEDIA_FOLDER), NoOpCacheEvictor(), get<DatabaseProvider>())
How can I avoid conflicts between buffering cache and downloaded files?
I had this issue also, and found the solution!
The downloading documentation states
The CacheDataSource.Factory should be configured as read-only to avoid downloading that content as well during playback.
To do this you must call setCacheWriteDataSinkFactory(null) on your CacheDataSource.Factory object.
This will prevent the stream from writing to the cache, allowing the downloader to write as expected.
I am using interactive video broadcasting in my app.
I am attaching class in which I am using live streaming.
I am getting the audio issue when I go back from the live streaming screen to the previous screen. I still listen to the audio of the host.
previously I was using leave channel method and destroying rtc client object, but after implementing this when I go back from streaming class then it closes all users screen who are using this app because of leave channel method. after that, I removed this option from my on destroy method.
Now I am using disable audio method which disables the audio but when I open live streaming class it doesn't enable audio. Enable audio method is not working I also used the mute audio local stream method and rtc handler on user mute audio method.
I am getting error--
"LiveStreamingActivity has leaked IntentReceiver io.agora.rtc.internal.AudioRoutingController$HeadsetBroadcastReceiver#101a7a7
that was originally registered here. Are you missing a call to
unregisterReceiver()? android.app.IntentReceiverLeaked: Activity
com.allin.activities.home.homeActivities.LiveStreamingActivity has
leaked IntentReceiver
io.agora.rtc.internal.AudioRoutingController$HeadsetBroadcastReceiver#101a7a7
that was originally registered here. Are you missing a call to
unregisterReceiver()?"
Receiver is registering in SDK and exception is coming inside the SDK that is jar file I can't edit.
Please help this in resolving my issue as I have to live the app on
play store.
//firstly I have tried this but it automatically stops other
devices streaming.
override fun onDestroy() {
/* if (mRtcEngine != null) {
leaveChannel()
RtcEngine.destroy(mRtcEngine)
mRtcEngine = null
}*/
//second I have tried disabling the audio so that user will
not hear
the host voice
if (mRtcEngine != null) //
{
mRtcEngine!!.disableAudio()
}
super.onDestroy()
}
// then I when I came back from the previous screen to live streaming activity everything is initializing again but the audio is not able to audible.
override fun onResume() {
super.onResume()
Log.e("resume", "resume")
if (mRtcEngine != null) {
mRtcEngine!!.enableAudio()
// mRtcEngine!!.resumeAudio()
}
}
code I am using
//agora rtc engine and handler initialization-----------------
private var mRtcEngine: RtcEngine? = null
private var mRtcEventHandler = object : IRtcEngineEventHandler() {
#SuppressLint("LongLogTag")
override fun onFirstRemoteVideoDecoded(uid: Int, width: Int,
height: Int, elapsed: Int) {
}
override fun onUserOffline(uid: Int, reason: Int) {
runOnUiThread {
val a = reason //if login =0 user is offline
try {
if (mUid == uid) {
if (surfaceView?.parent != null)
(surfaceView?.parent as ViewGroup).removeAllViews()
if (mRtcEngine != null) {
leaveChannel()
RtcEngine.destroy(mRtcEngine)
mRtcEngine = null
}
setResult(IntentConstants.REQUEST_CODE_LIVE_STREAMING)
finish()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onUserMuteVideo(uid: Int, muted: Boolean) {
runOnUiThread {
// onRemoteUserVideoMuted(uid, muted);
Log.e("video","muted")
}
}
override fun onAudioQuality(uid: Int, quality: Int, delay:
Short, lost: Short) {
super.onAudioQuality(uid, quality, delay, lost)
Log.e("", "")
}
override fun onUserJoined(uid: Int, elapsed: Int) {
// super.onUserJoined(uid, elapsed)
mUid = uid
runOnUiThread {
try {
setupRemoteVideo(mUid!!)
} catch (e: Exception) {
e.printStackTrace()
}
}
Log.e("differnt_uid----", mUid.toString())
}
}
private fun initAgoraEngineAndJoinChannel() {
if(mRtcEngine==null)
{
initializeAgoraEngine()
setupVideoProfile()
}
}
//initializing rtc engine class
#Throws(Exception::class)
private fun initializeAgoraEngine() {
try {
var s = RtcEngine.getSdkVersion()
mRtcEngine = RtcEngine.create(baseContext, AgoraConstants.APPLICATION_ID, mRtcEventHandler)
} catch (e: Exception) {
// Log.e(LOG_TAG, Log.getStackTraceString(e));
throw RuntimeException("NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e))
}
}
#Throws(Exception::class)
private fun setupVideoProfile() {
//mRtcEngine?.muteAllRemoteAudioStreams(true)
// mLogger.log("channelName account = " + channelName + ",uid = " + 0);
mRtcEngine?.enableVideo()
//mRtcEngine.clearVideoCompositingLayout();
mRtcEngine?.enableLocalVideo(false)
mRtcEngine?.setEnableSpeakerphone(false)
mRtcEngine?.muteLocalAudioStream(true)
joinChannel()
mRtcEngine?.setVideoProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING, true)
mRtcEngine?.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING)
mRtcEngine?.setClientRole(Constants.CLIENT_ROLE_AUDIENCE,"")
val speaker = mRtcEngine?.isSpeakerphoneEnabled
val camerafocus = mRtcEngine?.isCameraAutoFocusFaceModeSupported
Log.e("", "")
}
#Throws(Exception::class)
private fun setupRemoteVideo(uid: Int) {
val container = findViewById<FrameLayout>(R.id.fl_video_container)
if (container.childCount >= 1) {
return
}
surfaceView = RtcEngine.CreateRendererView(baseContext)
container.addView(surfaceView)
mRtcEngine?.setupRemoteVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_HIDDEN, uid))
mRtcEngine?.setRemoteVideoStreamType(uid, 1)
mRtcEngine?.setCameraAutoFocusFaceModeEnabled(false)
mRtcEngine?.muteRemoteAudioStream(uid, false)
mRtcEngine?.adjustPlaybackSignalVolume(0)
// mRtcEngine.setVideoProfile(Constants.VIDEO_PROFILE_180P, false); // Earlier than 2.3.0
surfaceView?.tag = uid // for mark purpose
val audioManager: AudioManager =
this#LiveStreamingActivity.getSystemService(Context.AUDIO_SERVICE) as AudioManager
//audioManager.mode = AudioManager.MODE_IN_CALL
val isConnected: Boolean = audioManager.isWiredHeadsetOn
if (isConnected) {
/* audioManager.isSpeakerphoneOn = false
audioManager.isWiredHeadsetOn = true*/
mRtcEngine?.setEnableSpeakerphone(false)
mRtcEngine?.setDefaultAudioRoutetoSpeakerphone(false)
mRtcEngine?.setSpeakerphoneVolume(0)
mRtcEngine?.enableInEarMonitoring(true)
// Sets the in-ear monitoring volume to 50% of original volume.
mRtcEngine?.setInEarMonitoringVolume(200)
mRtcEngine?.adjustPlaybackSignalVolume(200)
} else {
/* audioManager.isSpeakerphoneOn = true
audioManager.isWiredHeadsetOn = false*/
mRtcEngine?.setEnableSpeakerphone(true)
mRtcEngine?.setDefaultAudioRoutetoSpeakerphone(true)
mRtcEngine?.setSpeakerphoneVolume(50)
mRtcEngine?.adjustPlaybackSignalVolume(50)
mRtcEngine?.enableInEarMonitoring(false)
// Sets the in-ear monitoring volume to 50% of original volume.
mRtcEngine?.setInEarMonitoringVolume(0)
}
Log.e("", "")
}
#Throws(Exception::class)
private fun joinChannel() {
mRtcEngine?.joinChannel(
null,
AgoraConstants.CHANNEL_NAME,
"Extra Optional Data",
0
) // if you do not specify the uid, we will generate the uid for you
}
#Throws(Exception::class)
private fun leaveChannel() {
mRtcEngine!!.leaveChannel()
}
I think first you want to put setupRemoteVideo in onFirstRemoteVideoDecoded callback instead of the onUserJoined callback. Also, in the onDestroy callback, you should call RtcEngine.destroy() instead of RtcEngine.destroy(mRtcEngine).
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
How can I use ExoPlayer to play a video file, while also downloading it to some filepath ?
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:
If a file was cached, how can I use it in a safe manner?
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.
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:
Simply shared through email the video using the FAB. I received the video and was able to play it.
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.
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))
}
}
}
you can use exoplayer's SimpleCache with LeastRecentlyUsedCacheEvictor to cache while streaming. Code would look something like.
temporaryCache = new SimpleCache(new File(context.getExternalCacheDir(), "player"), new LeastRecentlyUsedCacheEvictor(bytesToCache));
cacheSourceFactory = new CacheDataSourceFactory(temporaryCache, buildDataSourceFactory(), CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR);