Android平台如何高效移动RTMP|RTSP直播流的录像文件?

📌 背景说明

在基于大牛直播SDK的 Android 应用中,录像功能常常用于本地保存 RTSP/RTMP 流媒体数据,生成 .mp4 文件以便后续回看、上传或编辑。我们的录像调用如下:

ini 复制代码
/* SmartPlayer.java
 * Created by daniusdk.com
 * WeChat: xinsheng120
 */
private void toggleRecording() {
	if (isRecording) {
		stopRecording();
	} else {
		startRecording();
	}
}

private void startRecording() {
	if (!isPlaying) {
		InitAndSetConfig();
	}

	ConfigRecorderParam();

	int ret = libPlayer.SmartPlayerStartRecorder(playerHandle);
	if (ret != 0) {
		Log.e(TAG, "Failed to start recorder.");
		return;
	}

	updateUIOnRecording(true);
	isRecording = true;
	btnStartStopRecorder.setText("停止录像");
}

private void stopRecording() {
	int ret = libPlayer.SmartPlayerStopRecorder(playerHandle);
	if (ret != 0) {
		Log.e(TAG, "Call SmartPlayerStopRecorder failed..");
		return;
	}

	if (!isPlaying) {
		libPlayer.SmartPlayerClose(playerHandle);
		playerHandle = 0;
	}

	updateUIOnRecording(false);
	isRecording = false;
	btnStartStopRecorder.setText("开始录像");
}

private void updateUIOnRecording(boolean recording) {
	boolean enable = !recording;
	btnPopInputUrl.setEnabled(enable);
	btnPopInputKey.setEnabled(enable);
	btnSetPlayBuffer.setEnabled(enable);
	btnFastStartup.setEnabled(enable);
	btnRecorderMgr.setEnabled(enable);
	btnReviewSnapshots.setEnabled(enable);
}

录制完成后,我们会有录像完成回调事件上来,并给出来当前录像文件完整的路径和文件名(如下代码),不分场景下,开发者会将这些录像文件从临时目录(如 /sdcard/daniulive/record_temp/)移动至正式目录(如 /sdcard/daniulive/record_saved/)进行统一管理。

ini 复制代码
class PlayerEventHandleV2 implements NTSmartEventCallbackV2 {
	@Override
	public void onNTSmartEventCallbackV2(long handle, int id, long param1,
										 long param2, String param3, String param4, Object param5) {

		String player_event = "";

		switch (id) {
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STARTED:
				player_event = "开始..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTING:
				player_event = "连接中..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTION_FAILED:
				player_event = "连接失败..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CONNECTED:
				player_event = "连接成功..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_DISCONNECTED:
				player_event = "连接断开..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_STOP:
				player_event = "停止播放..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RESOLUTION_INFO:
				player_event = "分辨率信息: width: " + param1 + ", height: " + param2;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_NO_MEDIADATA_RECEIVED:
				player_event = "收不到媒体数据,可能是url错误..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_SWITCH_URL:
				player_event = "切换播放URL..";
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_CAPTURE_IMAGE:
				player_event = "快照: " + param1 + " 路径:" + param3;

				if (param1 == 0)
					player_event = player_event + ", 截取快照成功";
				else
					player_event = player_event + ", 截取快照失败";

				if (param4 != null && !param4.isEmpty())
					player_event += (", user data:" + param4);

				break;

			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_RECORDER_START_NEW_FILE:
				player_event = "[record]开始一个新的录像文件 : " + param3;
				break;
			case NTSmartEventID.EVENT_DANIULIVE_ERC_PLAYER_ONE_RECORDER_FILE_FINISHED:
				player_event = "[record]已生成一个录像文件 : " + param3;
				break;
			....
		}

		if (player_event.length() > 0) {
			Log.i(TAG, player_event);
			Message message = new Message();
			message.what = PLAYER_EVENT_MSG;
			message.obj = player_event;
			handler.sendMessage(message);
		}
	}
}

然而,许多开发者默认采用如下做法:

copyFile(srcFile, destFile); srcFile.delete();

这种"复制+删除"的方式虽然通用,但在移动大文件(如 500MB~2GB)的场景下效率极低,尤其在老旧设备或外部 SD 卡路径下,会明显影响用户体验。

⚠️ 问题分析:为什么"复制+删除"效率低?

  1. 涉及完整 I/O 流读写:数据要从磁盘读取,再写入新位置,占用 CPU 与 I/O 资源;

  2. 受存储介质性能限制:在 SD 卡、低端闪存等设备上,写入速度可能低于 10MB/s;

  3. 额外耗电和发热:复制大文件过程中,系统资源高负荷运转;

  4. 浪费时间:1GB 文件复制可能需要 5~30 秒。

✅ 推荐方案:renameTo() + 同分区目录规划

Android 文件系统(基于 Linux)中提供的 rename() 系统调用,在同一分区下移动文件是极快的:

  • ✅ 不复制文件内容;

  • ✅ 实质仅修改文件路径元数据(inode 表);

  • ✅ 即使是 10GB 文件,也能 毫秒级完成

🌟 示例代码(重命名即移动):

🧠 renameTo() 的适用条件

条件

是否满足

✅ 源文件和目标路径必须在同一挂载点/分区

✅ 目标路径必须存在,且不会自动创建目录

否(需手动建目录)

✅ 文件或文件夹不能被占用(如未关闭文件流)

📌 判断是否在同一分区,可通过比较文件路径的 StatFs.getBlockDeviceName() 或直接在应用初始化阶段固定路径规划,确保一致性。

🛠 工具函数封装(推荐使用)

ini 复制代码
File srcFile = new File("/sdcard/daniulive/record_temp/video_20250601_0001.mp4");
File destFile = new File("/sdcard/daniulive/record_saved/video_20250601_0001.mp4");

boolean success = srcFile.renameTo(destFile);

if (success) {
    Log.i("MoveFile", "录像文件移动成功");
} else {
    Log.e("MoveFile", "移动失败,可能是跨分区");
}

💡 实践建议

  1. 规划目录结构 :确保录像临时目录和目标目录位于 /sdcard/ 下或 App 内同一逻辑存储(如 getExternalFilesDir());

  2. 避免频繁复制大文件 :除非是跨分区或外部存储之间,否则优先考虑 renameTo()

  3. 跨分区时,可异步处理:若确实需要跨分区复制,可在后台线程执行,并展示进度;

  4. 结合文件状态管理 :例如录像过程中命名为 .tmp,移动后改为正式 .mp4,更利于调试与维护。

📊 性能对比测试(实机数据)

操作类型

文件大小

操作耗时(Pixel 5, 内存存储)

renameTo()(同分区)

1GB

~10ms

copy + delete

1GB

~3.5秒

renameTo()(100个文件)

共1GB

~300ms

copy + delete(100文件)

共1GB

~7秒

✅ 总结

场景

推荐操作

同分区移动

renameTo()

跨分区移动

复制+删除,建议异步

多文件

批量 rename 操作 + 合理目录管理

通过合理使用 renameTo() 方法,结合大牛直播SDK录像输出路径规划,能极大提升文件移动效率与用户体验,是 文件管理逻辑不可忽视的性能优化点

📥 附加:完整目录管理实用类

java 复制代码
/* FileMover.java
 * Created by daniusdk.com
 * WeChat: xinsheng120
 */
import android.os.Build;
import android.os.StatFs;
import android.util.Log;

import java.io.*;

public class FileMover {

    public interface MoveCallback {
        void onSuccess(File src, File dest);
        void onFailure(File src, File dest, String reason);
    }

    /**
     * 高效移动文件:同分区使用 renameTo,跨分区复制再删除
     *
     * @param srcFile       原文件
     * @param destDir       目标目录(必须为目录)
     * @param overwrite     是否覆盖同名文件
     * @param backupSource  是否在跨分区复制时保留源文件
     * @param callback      操作回调
     */
    public static void moveFile(File srcFile, File destDir, boolean overwrite, boolean backupSource, MoveCallback callback) {
        if (srcFile == null || !srcFile.exists() || !srcFile.isFile()) {
            if (callback != null) callback.onFailure(srcFile, null, "源文件无效");
            return;
        }

        if (destDir == null || (!destDir.exists() && !destDir.mkdirs())) {
            if (callback != null) callback.onFailure(srcFile, null, "目标目录创建失败");
            return;
        }

        File destFile = new File(destDir, srcFile.getName());

        // 同名处理
        if (destFile.exists()) {
            if (overwrite) {
                destFile.delete();
            } else {
                destFile = getUniqueFile(destDir, srcFile.getName());
            }
        }

        boolean sameVolume = isSameVolume(srcFile, destDir);

        try {
            boolean success;

            if (sameVolume) {
                success = srcFile.renameTo(destFile);
            } else {
                success = copyFile(srcFile, destFile);
                if (success && !backupSource) {
                    success = srcFile.delete();
                }
            }

            if (success) {
                if (callback != null) callback.onSuccess(srcFile, destFile);
            } else {
                if (callback != null) callback.onFailure(srcFile, destFile, "文件移动失败");
            }

        } catch (Exception e) {
            if (callback != null) callback.onFailure(srcFile, destFile, "异常: " + e.getMessage());
        }
    }

    private static File getUniqueFile(File dir, String fileName) {
        File newFile = new File(dir, fileName);
        int count = 1;
        String name = fileName;
        String baseName = name;
        String ext = "";

        int dotIndex = name.lastIndexOf(".");
        if (dotIndex > 0) {
            baseName = name.substring(0, dotIndex);
            ext = name.substring(dotIndex);
        }

        while (newFile.exists()) {
            newFile = new File(dir, baseName + "_" + count + ext);
            count++;
        }
        return newFile;
    }

    private static boolean isSameVolume(File file1, File file2) {
        try {
            StatFs stat1 = new StatFs(file1.getAbsolutePath());
            StatFs stat2 = new StatFs(file2.getAbsolutePath());

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                return stat1.getBlockSizeLong() == stat2.getBlockSizeLong()
                        && stat1.getBlockCountLong() == stat2.getBlockCountLong();
            } else {
                return stat1.getBlockSize() == stat2.getBlockSize()
                        && stat1.getBlockCount() == stat2.getBlockCount();
            }
        } catch (Exception e) {
            return false;
        }
    }

    private static boolean copyFile(File src, File dest) {
        try (InputStream in = new FileInputStream(src);
             OutputStream out = new FileOutputStream(dest)) {

            byte[] buffer = new byte[8192];
            int length;

            while ((length = in.read(buffer)) > 0) {
                out.write(buffer, 0, length);
            }

            out.flush();
            return true;

        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }
}

调用示例如下:

arduino 复制代码
File src = new File("/sdcard/daniulive/record_temp/20250614_001.mp4");
File destDir = new File("/sdcard/daniulive/record_saved");

FileMover.moveFile(src, destDir, true, false, new FileMover.MoveCallback() {
    @Override
    public void onSuccess(File src, File dest) {
        Log.i("MoveFile", "移动成功: " + dest.getAbsolutePath());
    }

    @Override
    public void onFailure(File src, File dest, String reason) {
        Log.e("MoveFile", "移动失败: " + reason);
    }
});

相关推荐
Tiny_React4 小时前
使用 Claude Code Skills 模拟的视频生成流程
人工智能·音视频开发·vibecoding
aqi001 天前
FFmpeg开发笔记(九十八)基于FFmpeg的跨平台图形用户界面LosslessCut
android·ffmpeg·kotlin·音视频·直播·流媒体
aqi002 天前
FFmpeg开发笔记(九十七)国产的开源视频剪辑工具AndroidVideoEditor
android·ffmpeg·音视频·直播·流媒体
aqi003 天前
FFmpeg开发笔记(一百)国产的Android开源视频压缩工具VideoSlimmer
android·ffmpeg·音视频·直播·流媒体
haibindev5 天前
【终极踩坑指南】Windows 10上MsQuic证书加载失败?坑不在证书,而在Schannel!
直播·http3·quic·流媒体
飞鸟真人8 天前
livekit搭建与使用浏览器测试
直播·视频会议·视频聊天·livekit
hk11249 天前
【音视频/边缘计算】2025年度H.265/HEVC高并发解码与画质修复(Super-Resolution)基准测试报告(含沙丘/失控玩家核心样本)
ffmpeg·边缘计算·音视频开发·h.265·测试数据集
aqi0016 天前
FFmpeg开发笔记(九十五)国产的开源视频美颜工具VideoEditorForAndroid
android·ffmpeg·音视频·直播·流媒体
sno_guo17 天前
直播抠图技术100谈之17----相机帧率和直播帧率如何定?
直播·内容运营·抠图·直播运营·直播伴侣
李小轰_Rex19 天前
把手机变成听诊器!摄像头 30 秒隔空测心率 - 开箱即用
android·音视频开发