将元数据设置为 mp4

2024-04-22

我在以下的帮助下对视频进行编码MediaCodec and MediaMuxer。结果我有 mp4 视频文件。如何为此 mp4 文件设置元数据(创建时间)?媒体元数据检索器 http://developer.android.com/intl/ru/reference/android/media/MediaMetadataRetriever.htmlhttp://developer.android.com/intl/ru/reference/android/media/MediaMetadataRetriever.html只能读取元数据,但不能更改。我不想使用 ffmpeg。我试过mp4解析器 https://github.com/sannies/mp4parser图书馆 (这个班 https://github.com/sannies/mp4parser/blob/master/examples/src/main/java/com/googlecode/mp4parser/stuff/MetaDataTool.java),但它对我不起作用。


在 MP4 文件中设置元数据并不是一项明确的任务,因为没有普遍支持的规范 https://stackoverflow.com/a/33154416/1348726,但大多数视频播放器都支持 Apple 规格。

更多信息here https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW1, here http://atomicparsley.sourceforge.net/mpeg-4files.html, and here https://github.com/sannies/mp4parser/tree/5c67f810c3294d20d7fad8b2910f152237b9df19/isoparser/src/main/java/org/mp4parser/boxes/apple.

以下是在 MP4 元数据中设置标题和创建日期的代码(基于MetaDataInsert.java https://github.com/sannies/mp4parser/blob/master/examples/src/main/java/org/mp4parser/examples/metadata/MetaDataInsert.java样本):

import com.coremedia.iso.IsoFile;
import com.coremedia.iso.boxes.*;
import com.coremedia.iso.boxes.apple.AppleItemListBox;
import com.googlecode.mp4parser.boxes.apple.AppleNameBox;
import com.googlecode.mp4parser.boxes.apple.AppleRecordingYear2Box;
import com.googlecode.mp4parser.util.Path;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.util.List;

public class Mp4MetadataWriter {

    public FileChannel splitFileAndInsert(File f, long pos, long length) throws IOException {
        FileChannel read = new RandomAccessFile(f, "r").getChannel();
        File tmp = File.createTempFile("ChangeMetaData", "splitFileAndInsert");
        FileChannel tmpWrite = new RandomAccessFile(tmp, "rw").getChannel();
        read.position(pos);
        tmpWrite.transferFrom(read, 0, read.size() - pos);
        read.close();
        FileChannel write = new RandomAccessFile(f, "rw").getChannel();
        write.position(pos + length);
        tmpWrite.position(0);
        long transferred = 0;
        while ((transferred += tmpWrite.transferTo(0, tmpWrite.size() - transferred, write)) != tmpWrite.size()) {
            System.out.println(transferred);
        }
        System.out.println(transferred);
        tmpWrite.close();
        tmp.delete();
        return write;
    }


    private boolean needsOffsetCorrection(IsoFile isoFile) {
        if (Path.getPath(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 box : isoFile.getBoxes()) {
                if ("moov".equals(box.getType())) {
                    return true;
                }
                if ("mdat".equals(box.getType())) {
                    return false;
                }
            }
            throw new RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense");
        }
    }

    public void writeMetadata(String videoFilePath, String theTitle, String theDate) throws IOException {

        File videoFile = new File(videoFilePath);
        if (!videoFile.exists()) {
            throw new FileNotFoundException("File " + videoFilePath + " not exists");
        }

        if (!videoFile.canWrite()) {
            throw new IllegalStateException("No write permissions to file " + videoFilePath);
        }
        IsoFile isoFile = new IsoFile(videoFilePath);

        MovieBox moov = isoFile.getBoxes(MovieBox.class).get(0);
        FreeBox freeBox = findFreeBox(moov);

        boolean correctOffset = needsOffsetCorrection(isoFile);
        long sizeBefore = moov.getSize();
        long offset = 0;
        for (Box box : isoFile.getBoxes()) {
            if ("moov".equals(box.getType())) {
                break;
            }
            offset += box.getSize();
        }

        // Create structure or just navigate to Apple List Box.
        UserDataBox userDataBox;
        if ((userDataBox = Path.getPath(moov, "udta")) == null) {
            userDataBox = new UserDataBox();
            moov.addBox(userDataBox);
        }
        MetaBox metaBox;
        if ((metaBox = Path.getPath(userDataBox, "meta")) == null) {
            metaBox = new MetaBox();
            HandlerBox hdlr;
            hdlr = new HandlerBox();
            hdlr.setHandlerType("mdir");
            metaBox.addBox(hdlr);
            userDataBox.addBox(metaBox);
        }
        AppleItemListBox ilst;
        if ((ilst = Path.getPath(metaBox, "ilst")) == null) {
            ilst = new AppleItemListBox();
            metaBox.addBox(ilst);

        }
        if (freeBox == null) {
            freeBox = new FreeBox(128 * 1024);
            metaBox.addBox(freeBox);
        }

        // Got Apple List Box

        AppleNameBox nam;
        if ((nam = Path.getPath(ilst, AppleNameBox.TYPE)) == null) {
            nam = new AppleNameBox();
        }
        nam.setDataCountry(0);
        nam.setDataLanguage(0);
        nam.setValue(theTitle);
        ilst.addBox(nam);

        AppleRecordingYear2Box day;
        if ((day = Path.getPath(ilst, "©day")) == null) {
            day = new AppleRecordingYear2Box();
        }
        day.setDataCountry(0);
        day.setDataLanguage(0);
        day.setValue(theDate);
        ilst.addBox(day);

        long sizeAfter = moov.getSize();
        long diff = sizeAfter - sizeBefore;
        // This is the difference of before/after

        // can we compensate by resizing a Free Box we have found?
        if (freeBox.getData().limit() > diff) {
            // either shrink or grow!
            freeBox.setData(ByteBuffer.allocate((int) (freeBox.getData().limit() - diff)));
            sizeAfter = moov.getSize();
            diff = sizeAfter - sizeBefore;
        }
        if (correctOffset && diff != 0) {
            correctChunkOffsets(moov, diff);
        }
        BetterByteArrayOutputStream baos = new BetterByteArrayOutputStream();
        moov.getBox(Channels.newChannel(baos));
        isoFile.close();
        FileChannel fc;
        if (diff != 0) {
            // 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
            fc = splitFileAndInsert(videoFile, offset, sizeAfter - sizeBefore);
        } else {
            // simple overwrite of something with the file
            fc = new RandomAccessFile(videoFile, "rw").getChannel();
        }
        fc.position(offset);
        fc.write(ByteBuffer.wrap(baos.getBuffer(), 0, baos.size()));
        fc.close();
    }

    FreeBox findFreeBox(Container c) {
        for (Box box : c.getBoxes()) {
            System.err.println(box.getType());
            if (box instanceof FreeBox) {
                return (FreeBox) box;
            }
            if (box instanceof Container) {
                FreeBox freeBox = findFreeBox((Container) box);
                if (freeBox != null) {
                    return freeBox;
                }
            }
        }
        return null;
    }

    private void correctChunkOffsets(MovieBox movieBox, long correction) {
        List<ChunkOffsetBox> chunkOffsetBoxes = Path.getPaths((Box) movieBox, "trak/mdia[0]/minf[0]/stbl[0]/stco[0]");
        if (chunkOffsetBoxes.isEmpty()) {
            chunkOffsetBoxes = Path.getPaths((Box) movieBox, "trak/mdia[0]/minf[0]/stbl[0]/st64[0]");
        }
        for (ChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
            long[] cOffsets = chunkOffsetBox.getChunkOffsets();
            for (int i = 0; i < cOffsets.length; i++) {
                cOffsets[i] += correction;
            }
        }
    }

    private static class BetterByteArrayOutputStream extends ByteArrayOutputStream {
        byte[] getBuffer() {
            return buf;
        }
    }

}

Usage:

new Mp4MetadataWriter().writeMetadata("/home/user/downloads/1.mp4", "Yet another video title", "2020");

Result: VLC Media Information Dialog

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

将元数据设置为 mp4 的相关文章

  • 使用新语法应用 Android Gradle 插件

    如何使用新的 Gradle 插件语法应用 Android 插件 plugins id version 代替 buildscript dependencies classpath com android tools build gradle
  • 将项目升级到Android Studio 1.0(Gradle问题)

    首先 我对 android 开发 android studio gradle 非常陌生 所以如果我问了一个愚蠢的问题 请原谅我 我的团队一直在使用 android studio 的 beta 版本开发一个项目 我刚刚安装了新版本 1 0 并
  • 适用于 IOS 和 Android 的支付网关 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我正在开发一个应用程序 用户必须在澳大利亚餐馆通过应用程序 android ios 付款 有两种付款方式 通过 PayPal 或 Visa
  • Android:我可以创建一个不是矩形的视图/画布吗?圆形的?

    我有一个圆形视图 悬停在主要内容上方 gt 从屏幕出来的 z 轴方向 当有人点击屏幕时 我希望选择主要内容或悬停在上方的视图 当它覆盖主视图时 到目前为止效果很好 我在透明画布上有一个圆形物品 这意味着您可以看到该圆圈之外的背景的所有内容
  • 更改卡片高度即更改 Jetpack 中与 Material 3 组合的卡片颜色

    我正在使用 Card 可组合项 我希望它的颜色为白色 但是当我向它添加一些高度时 它的颜色会更改为更像主要容器颜色 我看过文档 其中有一种称为高程覆盖的东西 但找不到说明如何使用它的示例 这是我的代码 Card modifier Modif
  • 位图内存不足错误

    我对这个错误有疑问 我从 URL 制作网站图标解析器 我这样做是这样的 public class GrabIconsFromWebPage public static String replaceUrl String url StringB
  • 具有自定义源集的 Android Gradle 风格 - gradle 文件应该是什么样子?

    我有一个旧的 eclipse 项目 我已经转移到 android studio 并设置为使用flavor 它似乎工作得很好 直到我开始尝试在我的风格之间使用不同的 java 文件 我的项目设置是这样的 ProjectRoot acitonb
  • 如何向开发人员发送崩溃报告?

    我开发 Android 应用程序 但在某些情况下我的应用程序force close 如果出现以下情况 我如何向开发人员发送包含详细信息的电子邮件force close随时发生 The ACRA https github com ACRA a
  • 我想从 android 中服务器的视频 url 创建缩略图

    My code public static Bitmap retriveVideoFrameFromVideo String videoPath throws Throwable Bitmap bitmap null MediaMetada
  • 在选项卡上保存数据

    我有 3 个选项卡 每个选项卡都有一个单独的活动 我想在用户单击任一选项卡上的 保存 时保存数据 有几个选项可供选择 共享首选项 全局变量或将对象保存在上下文中 编辑 我必须保存图像和文本字段 Android 共享首选项 https sta
  • 像 WhatsApp 一样发送图片

    我做了一个聊天应用程序 我想添加照片 文件共享我的应用程序中的概念与 WhatsApp 相同 我已经使用该应用程序制作了Xmpp Openfire目前我正在使用此功能进行照片共享 但它并不完全可靠 public void sendFile
  • Android 纹理仅显示纯色

    我正在尝试在四边形上显示单个纹理 我有一个可用的 VertexObject 它可以很好地绘制一个正方形 或任何几何对象 现在我尝试扩展它来处理纹理 但纹理不起作用 我只看到一种纯色的四边形 坐标数据位于 arrayList 中 the ve
  • 如何使用应用程序接口将蓝牙套接字传递给另一个活动

    因此 根据我收集的信息 套接字连接既不可序列化 也不可分割 但我需要将蓝牙连接传递给另一个活动 我不想作为中间人编写服务 所以请不要将此作为解决方案发布 我听说有一种方法可以使用自定义应用程序接口来传递这些类型的对象 但我一生都找不到这样的
  • Jetpack 导航:如何从一个嵌套图的子级导航到另一个嵌套图的子级?

    导航结构 MainActivity nav root HomeFragment AuthNestedGraph nav auth BeforeOtpFragment home OtpFragment ProfileNestedGraph n
  • Glass 语音命令给定列表中最接近的匹配项

    使用 Glass 您可以通过 确定 Glass 菜单启动应用程序 它似乎会选择最接近的匹配项 除非命令相距数英里 并且您可以明显看到命令列表 无论如何 是否可以从应用程序内或从语音提示 在初始应用程序触发后 给出类似的列表并返回最接近的匹配
  • 是否可以使用 CardView 为浮动操作按钮制作阴影?

    I know CardView不是为此而设计的 但理论上如果cardCornerRadius view size 2它应该导致圆圈 我错过了什么吗 绘制真实的动画阴影并不困难 您可以尝试在 Froyo 等任何 Android 设备上实现 L
  • 使用 DataBindingComponent 的 Inflate 方法

    当 Glide 成功渲染图像后 我在更新文本视图时看到此错误 致命异常 java lang IllegalStateException 必需 CustomBinding 类中的 DataBindingComponent 为 null 绑定适
  • 使用Intent拨打电话需要权限吗?

    在我的一个应用程序中 我使用以下代码来拨打电话 Intent intent new Intent Intent ACTION CALL Uri parse startActivity intent 文档说我确实需要以下清单许可才能这样做
  • Flash 对象未显示在phonegap android 中

    我已经在 android 手机间隙创建了一个应用程序 我有一个屏幕 我想显示一个静态 flash obj 所以我在屏幕 HTML 页面中放入了以下代码
  • 为什么带处理程序的连续自动对焦相机不允许切换相机闪光灯?

    到目前为止我所做的 我已经实现了用于读取二维码的自定义相机 需要继续聚焦相机以获得更好的二维码读取 我的问题当我使用处理程序每 秒聚焦一次时 相机闪光灯开 关按钮不起作用 或者打开和关闭相机闪光灯需要太多时间 当我删除每秒自动对焦相机的代码

随机推荐