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
}
}
}
Related
I'm currently trying to create a web application in spring boot, and i need to copy some codes from another android application, and i can't copy this function SystemClock.elapsedRealtime() , because it's a function that exist by default in android, that's why i need an alternative for this function in spring boot please.
I tried to find alternatives for this functions using packages in github, but didn't found anything good, i tried to use the Clock class in java but didn't have what i want, this function is supposed to return milliseconds since boot, including time spent in sleep, how can i replace it in srping boot application.
This is the code i'm copying is a ratelimitinterceptor for Okhttp library :
import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.util.ArrayDeque
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
fun OkHttpClient.Builder.rateLimit(
permits: Int,
period: Long = 1,
unit: TimeUnit = TimeUnit.SECONDS,
) = addInterceptor(RateLimitInterceptor(null, permits, period, unit))
#Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
internal class RateLimitInterceptor(
private val host: String?,
private val permits: Int,
period: Long,
unit: TimeUnit,
) : Interceptor {
private val requestQueue = ArrayDeque<Long>(permits)
private val rateLimitMillis = unit.toMillis(period)
private val fairLock = Semaphore(1, true)
override fun intercept(chain: Interceptor.Chain): Response {
val call = chain.call()
if (call.isCanceled()) throw IOException("Canceled")
val request = chain.request()
when (host) {
null, request.url.host -> {} // need rate limit
else -> return chain.proceed(request)
}
try {
fairLock.acquire()
} catch (e: InterruptedException) {
throw IOException(e)
}
val requestQueue = this.requestQueue
val timestamp: Long
try {
synchronized(requestQueue) {
while (requestQueue.size >= permits) { // queue is full, remove expired entries
val periodStart = SystemClock.elapsedRealtime() - rateLimitMillis
var hasRemovedExpired = false
while (requestQueue.isEmpty().not() && requestQueue.first <= periodStart) {
requestQueue.removeFirst()
hasRemovedExpired = true
}
if (call.isCanceled()) {
throw IOException("Canceled")
} else if (hasRemovedExpired) {
break
} else try { // wait for the first entry to expire, or notified by cached response
(requestQueue as Object).wait(requestQueue.first - periodStart)
} catch (_: InterruptedException) {
continue
}
}
// add request to queue
timestamp = SystemClock.elapsedRealtime()
requestQueue.addLast(timestamp)
}
} finally {
fairLock.release()
}
val response = chain.proceed(request)
if (response.networkResponse == null) { // response is cached, remove it from queue
synchronized(requestQueue) {
if (requestQueue.isEmpty() || timestamp < requestQueue.first) return#synchronized
requestQueue.removeFirstOccurrence(timestamp)
(requestQueue as Object).notifyAll()
}
}
return response
}
}
I want Google Speech to text API to recognize a short phrase after I press a button. So I came up with the following code. But it keeps returning no results. I'm quite confused, there are results in there (the buffer etc.), the mic is working well and is enabled in the emulator. Google console also doesn't show errors.
Here's my code.
Click listener that starts the recording:
val clicker: View.OnClickListener = View.OnClickListener {
Log.d(TAG, "Starting record thread")
mAudioRecorder.record(LISTEN_TIME_MILLIS)
}
mReadButton.setOnClickListener(clicker)
Here's a broadcast receiver that processes the results and tries to send them to Google:
private val broadCastReceiver = object : BroadcastReceiver() {
override fun onReceive(contxt: Context?, intent: Intent?) {
if (intent!!.getBooleanExtra(RECORDING_SUCCESS, false)) {
val byteArrayExtra = intent.getByteArrayExtra(RECORDING_AUDIO)
val audioResultByteString: ByteString = ByteString.copyFrom(byteArrayExtra)
if (audioResultByteString.size() > 0) {
val audio: RecognitionAudio = RecognitionAudio.newBuilder()
.setContent(audioResultByteString).build()
val resultsList = mSpeechClient.recognize(config, audio).resultsList
if (resultsList.size > 0) {
for (result in resultsList) {
val resultText = result.alternativesList[0].transcript
}
}
Log.d(TAG, "- Done recognition. Result Qty: ${resultsList.size}")
}
}
}
}
Here is the AudioRecorder class function, which does the recording:
fun record(listenTimeMillis: Long) {
val byteString: ByteString = ByteString.EMPTY
mAudioRecorder = initAudioRecorder()
val mBuffer = ByteArray(4 * AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, CHANNEL, ENCODING))
mAudioRecorder!!.startRecording()
Thread {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
Thread.sleep(listenTimeMillis)
val read = mAudioRecorder!!.read(mBuffer, 0, mBuffer.size, AudioRecord.READ_NON_BLOCKING)
val intent = Intent(RECORDING_COMPLETED_INTENT)
try {
if (read > 0) {
intent.putExtra(RECORDING_AUDIO, mBuffer)
intent.putExtra(RECORDING_SUCCESS, true)
}
LocalBroadcastManager.getInstance(context).sendBroadcast(intent)
} catch (e: Exception) {
Log.e(TAG, e.stackTrace.toString())
}
releaseAudioRecorder()
}.start()
}
I solved this. The thing to blame was a too small buffer size. So the recognition server was actually getting half a second of audio record which it obviously couldn't recognize.
val mBuffer = ByteArray(4 * AudioRecord.getMinBufferSize(SAMPLE_RATE_HZ, CHANNEL, ENCODING))
instead of 4 I put 200 and instead of AudioRecord.READ_NON_BLOCKING I have put AudioRecord.READ_BLOCKING and I read the buffer in a loop and increase the offset in each iteration. Then it started working.
val startTime = System.currentTimeMillis()
var deltaTime = 0L
var offset = 0
val intent = Intent(RECORDING_COMPLETED_INTENT)
val readChunk = 512
while (deltaTime < listenTimeMillis && offset < mBuffer.size) {
val read = mAudioRecord!!.read(mBuffer, offset, readChunk, AudioRecord.READ_BLOCKING)
if (read < 0) {
intent.putExtra(RECORDING_SUCCESS, false)
break; //if read with error, end here
}
deltaTime = System.currentTimeMillis() - startTime //startTime is a while loop breaking condition so it lestens only for specified amount of time
offset += readChunk
}
I'm making an app where I play .mp3 files from the URL. I'm using the latest version of ExoPlayer 2.11.4.
What I need to do is get the total duration of the audio from the url so I can use it in my custom audio player.
The urls I'm using are of this type: https://myserver.net/.../audio/439688e0c3626d39d3ef3.mp3?token=6oz-22-2sa-9yh-7e-2iak
The problem is that sometimes my code works correctly most of the time and returns the correct duration. But sometimes what I get is a negative number: -9223372036854775807
And that doesn't allow my code to work properly. My code where I get the duration is this:
fun getDuration(url: String, context: Context) {
exoPlayer?.release()
exoPlayer = SimpleExoPlayer.Builder(context).build()
val dataSourceFactory = DefaultDataSourceFactory(context, Util.getUserAgent(context, "ExoPlayer"))
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(url))
exoPlayer?.prepare(mediaSource)
exoPlayer?.addListener(object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
if (playbackState == ExoPlayer.STATE_READY) {
val realDurationMillis: Long? = exoPlayer?.getDuration()
currentDuration = realDurationMillis
if (currentDuration != null) {
if (currentDuration!! > 0) {
exoPlayer?.release()
}
}
}
}
})
}
C.TIME_UNSET is defined as Long.MIN_VALUE + 1 which is where your -9223372036854775807 comes from.
/**
* Special constant representing an unset or unknown time or duration. Suitable for use in any
* time base.
*/
public static final long TIME_UNSET = Long.MIN_VALUE + 1;
I have created BLE sender class for the sending large ByteArray via Bluetooth LE
The logic of the send process following:
Write descriptor to enable notification on characteristics that sends data
Notify peripheral about data sending process via
writing to corresponding characteristics (Size of data: chunk size:
number of chunks)
Wait for peripheral to notify for chunk 0 to send on data sending characteristics
On notification received start sending the first chunk 1000 byte by blocks of 20 bytes (BLE restriction) where each block contains block number and 18 bytes of data, after 1000 bytes sent, send block of checksum for the data sent
Peripheral verify the data by the checksum and notify descriptor for the next chunk
My Question is: is there any better approach?
I have found that writing characteristics multiple times requires some delay of at least 20 milliseconds. Is there any way to avoid this?
Changed the implementation instead of 20 millis, I'm waiting for a callback onCharacteristicWrite as
Emil advised. and Also changed the prepare method to decrease calculation time between 18bytes blocks sends:
class BluetoothLEDataSender(
val characteristicForSending: BluetoothGattCharacteristic,
val characteristicForNotifyDataSend: BluetoothGattCharacteristic,
private val config: BluetoothLESenderConfiguration = BluetoothLESenderConfiguration(),
val bluetoothLeService: WeakReference<BluetoothLeService>) : HandlerThread("BluetoothLEDataSender") {
data class BluetoothLESenderConfiguration(val sendingIntervalMillis: Long = 20L, val chunkSize: Int = 1000, val retryForFailureInSeconds: Long = 3)
private val toaster by lazy { Toast.makeText(bluetoothLeService.get()!!,"",Toast.LENGTH_SHORT) }
companion object {
val ACTION_DATA_SEND_FINISHED = "somatix.com.bleplays.ACTION_DATA_SEND_FINISHED"
val ACTION_DATA_SEND_FAILED = "somatix.com.bleplays.ACTION_DATA_SEND_FAILED"
}
lateinit var dataToSend: List<BlocksQueue>
val messageHandler by lazy { SenderHandler()}
var currentIndex = 0
public fun notifyDataState(receivedChecksum: String) {
val msg = Message()
msg.arg1 = receivedChecksum.toInt()
messageHandler.sendMessage(msg)
}
inner class BlocksQueue(val initialCapacity:Int):ArrayBlockingQueue<ByteArray>(initialCapacity)
inner class BlockSendingTask:Runnable{
override fun run() {
executeOnUiThread({ toaster.setText("Executing block: $currentIndex")
toaster.show()})
sendNext()
}
}
public fun sendMessage(messageByteArray: ByteArray) {
start()
dataToSend = prepareSending(messageByteArray)
bluetoothLeService.get()?.setEnableNotification(characteristicForSending,true)
val descriptor = characteristicForSending.getDescriptor(DESCRIPTOR_CONFIG_UUID)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
bluetoothLeService.get()?.writeDescriptor(descriptor)
characteristicForNotifyDataSend.value = "${messageByteArray.size}:${config.chunkSize}:${dataToSend.size}".toByteArray()
toaster.setText(String(characteristicForNotifyDataSend.value))
toaster.show()
messageHandler.postDelayed({bluetoothLeService.get()?.writeCharacteristic(characteristicForNotifyDataSend)}, config.sendingIntervalMillis)
}
private fun prepareSending(messageByteArray: ByteArray): ArrayList<BlocksQueue> {
with(config)
{
var chunksNumber = messageByteArray.size / config.chunkSize
chunksNumber = if (messageByteArray.size == chunksNumber * config.chunkSize) chunksNumber else chunksNumber + 1
val chunksArray = ArrayList<BlocksQueue>()
(0 until chunksNumber).mapTo(chunksArray) {
val start = it * chunkSize
val end = if ((start + chunkSize) > messageByteArray.size) messageByteArray.size else start + chunkSize
val sliceArray = messageByteArray.sliceArray(start until end)
listOfCheckSums.add(sliceArray.checkSum())
var capacity = sliceArray.size / 18
capacity = if(sliceArray.size - capacity*18 == 0) capacity else capacity + 1
//Add place for checksum
val queue = BlocksQueue(capacity+1)
for(i in 0 until capacity){
val start1 = i *18
val end1 = if((start1 + 18)<sliceArray.size) start1 +18 else sliceArray.size
queue.add(sliceArray.sliceArray(start1 until end1))
}
queue.add(sliceArray.checkSum().toByteArray())
queue
}
return chunksArray
}
}
fun sendNext(){
val currentChunk = dataToSend.get(currentIndex)
val peek = currentChunk.poll()
if(peek != null)
{
if(currentChunk.initialCapacity > currentBlock+1)
{
val indexByteArray = if(currentBlock>9) "$currentBlock".toByteArray() else "0${currentBlock}".toByteArray()
characteristicForSending.value = indexByteArray + peek
}
else{
characteristicForSending.value = peek
}
bluetoothLeService.get()?.writeCharacteristic(characteristicForSending)
currentBlock++
}
else
{
Log.i(TAG, "Finished chunk $currentIndex")
currentBlock = 0
}
}
private val TAG= "BluetoothLeService"
#SuppressLint("HandlerLeak")
inner class SenderHandler:Handler(looper){
private var failureCheck:FailureCheck? = null
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
currentIndex = msg.arg1
if(currentIndex < dataToSend.size)
{
if (currentIndex!= 0 && failureCheck != null)
{
removeCallbacks(failureCheck)
}
failureCheck = FailureCheck(currentIndex)
post(BlockSendingTask())
postDelayed(failureCheck,TimeUnit.MILLISECONDS.convert(config.retryForFailureInSeconds,TimeUnit.SECONDS))
}
else {
if (currentIndex!= 0 && failureCheck != null)
{
removeCallbacks(failureCheck)
}
val intent= Intent(ACTION_DATA_SEND_FINISHED)
bluetoothLeService.get()?.sendBroadcast(intent)
}
}
private inner class FailureCheck(val index:Int):Runnable{
override fun run() {
if (index==currentIndex){
val intent= Intent(ACTION_DATA_SEND_FAILED)
bluetoothLeService.get()?.sendBroadcast(intent)
}
}
}
}
}
What's this thing about waiting 20 ms? The preferred way to pump data using characteristic writes is to first use "Write Without Response" (https://developer.android.com/reference/android/bluetooth/BluetoothGattCharacteristic.html#WRITE_TYPE_NO_RESPONSE), then perform a Write, then wait for the onCharacteristicWrite callback and then immediately perform the next Write. You need to wait for the onCharacteristicWrite callback since the API doesn't allow you to have multiple pending commands/requests at a time.
I work with #libinm (OP) on the same project and would like to refer to #Emil comment above -
#Emil mentioned that "API doesn't allow you to have multiple pending commands/requests at a time", I'm wondering if there is any kind of message buffering that enables to increase throughput (by sending multiple messages on a single BLE connection event). I know that much lighter (embedded) BLE stacks enable buffering of 4/6 messages (TI/Nordic stacks respectively) per connection event.
How will the Android BLE central respond to multiple message notifications per single connection event (sent by peripheral)? Are there any limitations?
Background
Android supports various audio files encoding and decoding.
I record audio into an audio file using android.media.MediaRecorder class, but I also wish to show information about the files I've recorded (not standard data, but still just text, maybe even configurable by user), and I think it's best to store this information within the files.
examples of possible data to store: when it was recorded, where it was recorded, notes by the user...
The problem
The MediaRecorder class doesn't have any function that I can find, to add or even read metadata of the recorded audio file.
I also can't find a similar class that does it.
What I've tried
I tried searching how to do it for specific files types, and also tried to find a library that does it.
I haven't find even a clue about this information.
The only thing I've found for MediaRecorder class, is a function called "setLocation" , which is used to indicate where the recording has started (geographically), and looking at its code, I can see it sets parameters:
public void setLocation(float latitude, float longitude) {
int latitudex10000 = (int) (latitude * 10000 + 0.5);
int longitudex10000 = (int) (longitude * 10000 + 0.5);
if (latitudex10000 > 900000 || latitudex10000 < -900000) {
String msg = "Latitude: " + latitude + " out of range.";
throw new IllegalArgumentException(msg);
}
if (longitudex10000 > 1800000 || longitudex10000 < -1800000) {
String msg = "Longitude: " + longitude + " out of range";
throw new IllegalArgumentException(msg);
}
setParameter("param-geotag-latitude=" + latitudex10000);
setParameter("param-geotag-longitude=" + longitudex10000);
}
But setParameter is private, and I'm not sure if it's ok to put anything I want into it, even if I had a way to access it (reflection, for example) :
private native void setParameter(String nameValuePair);
I also don't get, given an audio/video file, how to get/modify this kind of information. It's not available for SimpleExoPlayer, for example.
The questions
How can I read,write, and modify metadata inside supported audio files of Android?
Are there any limitations/restrictions for those actions?
Which file formats are available for this?
Is it possible to add the metadata while I record the audio?
Is it possible perhaps via MediaStore ? But then how do I do those operations? And which files are supported? And does the metadata stay within the file?
EDIT: ok I've looked at the solution offered to me (here, repo here, based on here) , and it seems to work well. However, it doesn't work on latest version of the library that it uses (org.mp4parser.isoparser:1.9.37 dependency of mp4parser) , so I leave this question to be answered : Why doesn't it work on latest version of this library?
Code:
object MediaMetaDataUtil {
interface PrepareBoxListener {
fun prepareBox(existingBox: Box?): Box
}
#WorkerThread
fun <T : Box> readMetadata(mediaFilePath: String, boxType: String): T? {
return try {
val isoFile = IsoFile(FileDataSourceImpl(FileInputStream(mediaFilePath).channel))
val nam = Path.getPath<T>(isoFile, "/moov[0]/udta[0]/meta[0]/ilst/$boxType")
isoFile.close()
nam
} catch (e: Exception) {
null
}
}
/**
* #param boxType the type of the box. Example is "©nam" (AppleNameBox.TYPE). More available here: https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/
* #param listener used to prepare the existing or new box
* */
#WorkerThread
#Throws(IOException::class)
fun writeMetadata(mediaFilePath: String, boxType: String, listener: PrepareBoxListener) {
val videoFile = File(mediaFilePath)
if (!videoFile.exists()) {
throw FileNotFoundException("File $mediaFilePath not exists")
}
if (!videoFile.canWrite()) {
throw IllegalStateException("No write permissions to file $mediaFilePath")
}
val isoFile = IsoFile(mediaFilePath)
val moov = isoFile.getBoxes<MovieBox>(MovieBox::class.java)[0]
var freeBox = findFreeBox(moov)
val correctOffset = needsOffsetCorrection(isoFile)
val sizeBefore = moov.size
var offset: Long = 0
for (box in isoFile.boxes) {
if ("moov" == box.type) {
break
}
offset += box.size
}
// Create structure or just navigate to Apple List Box.
var userDataBox: UserDataBox? = Path.getPath(moov, "udta")
if (userDataBox == null) {
userDataBox = UserDataBox()
moov.addBox(userDataBox)
}
var metaBox: MetaBox? = Path.getPath(userDataBox, "meta")
if (metaBox == null) {
metaBox = MetaBox()
val hdlr = HandlerBox()
hdlr.handlerType = "mdir"
metaBox.addBox(hdlr)
userDataBox.addBox(metaBox)
}
var ilst: AppleItemListBox? = Path.getPath(metaBox, "ilst")
if (ilst == null) {
ilst = AppleItemListBox()
metaBox.addBox(ilst)
}
if (freeBox == null) {
freeBox = FreeBox(128 * 1024)
metaBox.addBox(freeBox)
}
// Got Apple List Box
var nam: Box? = Path.getPath(ilst, boxType)
nam = listener.prepareBox(nam)
ilst.addBox(nam)
var sizeAfter = moov.size
var diff = sizeAfter - sizeBefore
// This is the difference of before/after
// can we compensate by resizing a Free Box we have found?
if (freeBox.data.limit() > diff) {
// either shrink or grow!
freeBox.data = ByteBuffer.allocate((freeBox.data.limit() - diff).toInt())
sizeAfter = moov.size
diff = sizeAfter - sizeBefore
}
if (correctOffset && diff != 0L) {
correctChunkOffsets(moov, diff)
}
val baos = BetterByteArrayOutputStream()
moov.getBox(Channels.newChannel(baos))
isoFile.close()
val fc: FileChannel = if (diff != 0L) {
// this is not good: We have to insert bytes in the middle of the file
// and this costs time as it requires re-writing most of the file's data
splitFileAndInsert(videoFile, offset, sizeAfter - sizeBefore)
} else {
// simple overwrite of something with the file
RandomAccessFile(videoFile, "rw").channel
}
fc.position(offset)
fc.write(ByteBuffer.wrap(baos.buffer, 0, baos.size()))
fc.close()
}
#WorkerThread
#Throws(IOException::class)
fun splitFileAndInsert(f: File, pos: Long, length: Long): FileChannel {
val read = RandomAccessFile(f, "r").channel
val tmp = File.createTempFile("ChangeMetaData", "splitFileAndInsert")
val tmpWrite = RandomAccessFile(tmp, "rw").channel
read.position(pos)
tmpWrite.transferFrom(read, 0, read.size() - pos)
read.close()
val write = RandomAccessFile(f, "rw").channel
write.position(pos + length)
tmpWrite.position(0)
var transferred: Long = 0
while (true) {
transferred += tmpWrite.transferTo(0, tmpWrite.size() - transferred, write)
if (transferred == tmpWrite.size())
break
//System.out.println(transferred);
}
//System.out.println(transferred);
tmpWrite.close()
tmp.delete()
return write
}
#WorkerThread
private fun needsOffsetCorrection(isoFile: IsoFile): Boolean {
if (Path.getPath<Box>(isoFile, "moov[0]/mvex[0]") != null) {
// Fragmented files don't need a correction
return false
} else {
// no correction needed if mdat is before moov as insert into moov want change the offsets of mdat
for (box in isoFile.boxes) {
if ("moov" == box.type) {
return true
}
if ("mdat" == box.type) {
return false
}
}
throw RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense")
}
}
#WorkerThread
private fun findFreeBox(c: Container): FreeBox? {
for (box in c.boxes) {
// System.err.println(box.type)
if (box is FreeBox)
return box
if (box is Container) {
val freeBox = findFreeBox(box as Container)
if (freeBox != null) {
return freeBox
}
}
}
return null
}
#WorkerThread
private fun correctChunkOffsets(movieBox: MovieBox, correction: Long) {
var chunkOffsetBoxes = Path.getPaths<ChunkOffsetBox>(movieBox as Box, "trak/mdia[0]/minf[0]/stbl[0]/stco[0]")
if (chunkOffsetBoxes.isEmpty())
chunkOffsetBoxes = Path.getPaths(movieBox as Box, "trak/mdia[0]/minf[0]/stbl[0]/st64[0]")
for (chunkOffsetBox in chunkOffsetBoxes) {
val cOffsets = chunkOffsetBox.chunkOffsets
for (i in cOffsets.indices)
cOffsets[i] += correction
}
}
private class BetterByteArrayOutputStream : ByteArrayOutputStream() {
val buffer: ByteArray
get() = buf
}
}
Sample usage for writing&reading title:
object MediaMetaData {
#JvmStatic
#Throws(IOException::class)
fun writeTitle(mediaFilePath: String, title: String) {
MediaMetaDataUtil.writeMetadata(mediaFilePath, AppleNameBox.TYPE, object : MediaMetaDataUtil.PrepareBoxListener {
override fun prepareBox(existingBox: Box?): Box {
var nam: AppleNameBox? = existingBox as AppleNameBox?
if (nam == null)
nam = AppleNameBox()
nam.dataCountry = 0
nam.dataLanguage = 0
nam.value = title
return nam
}
})
}
#JvmStatic
fun readTitle(mediaFilePath: String): String? {
return MediaMetaDataUtil.readMetadata<AppleNameBox>(mediaFilePath, AppleNameBox.TYPE)?.value
}
}
It seems there's no way to do it uniformly for all supported audio formats in Android. There are some limited options for particular formats though, so I suggest to stick with one format.
MP3 is the most popular one and there should be a lot of options like this one.
If you don't want to deal with encoding/decoding, there are some options for a WAV format.
There's also a way to add a metadata track to a MP4 container using MediaMuxer (you can have an audio-only MP4 file) or like this.
Regarding MediaStore: here's an example (at the end of page 318) on how to add metadata to it just after using MediaRecorder. Though as far as I know the data won't be recorded inside the file.
Update
I compiled an example app using this MP4 parser library and MediaRecorder example from SDK docs. It records an audio, puts it in MP4 container and adds String metadata like this:
MetaDataInsert cmd = new MetaDataInsert();
cmd.writeRandomMetadata(fileName, "lore ipsum tralalala");
Then on the next app launch this metadata is read and displayed:
MetaDataRead cmd = new MetaDataRead();
String text = cmd.read(fileName);
tv.setText(text);
Update #2
Regarding m4a file extension: m4a is just an alias for an mp4 file with AAC audio and has the same file format. So you can use my above example app and just change the file name from audiorecordtest.mp4 to audiorecordtest.m4a and change audio encoder from MediaRecorder.AudioEncoder.AMR_NB to MediaRecorder.AudioEncoder.AAC.