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
}
}