package com.yixia.camera; import android.media.ExifInterface; import android.media.MediaMetadataRetriever; import com.yixia.camera.model.MediaObject.MediaPart; import com.yixia.camera.util.DeviceUtils; import com.yixia.camera.util.FileUtils; import com.yixia.camera.util.Log; import com.yixia.camera.util.StringUtils; import com.yixia.videoeditor.adapter.UtilityAdapter; import java.io.File; import java.io.IOException; /** * ffmpeg工具类 * * @author yixia.com * */ public class FFMpegUtils { /** 音量 100% -vol 100 */ public static final float AUDIO_VOLUME_HIGH = 1F; /** 音量 66% */ public static final float AUDIO_VOLUME_MEDIUM = 0.66F; /** 音量 33% */ public static final float AUDIO_VOLUME_LOW = 0.33F; /** 音量 关闭 */ public static final int AUDIO_VOLUME_CLOSE = 0; /** FFMPEG输出log到logcat */ private static final String FFMPEG_COMMAND_LOG_LOGCATE = " -d stdout -loglevel verbose"; /** FFMPEG视频编码参数 */ private static final String FFMPEG_COMMAND_VCODEC = " -pix_fmt yuv420p -vcodec libx264 -profile:v baseline -preset ultrafast"; /** 获取log输出的文件路径 */ public static String getLogCommand() { if (VCamera.isLog()) return FFMPEG_COMMAND_LOG_LOGCATE; else return " -d \"" + VCamera.getVideoCachePath() + VCamera.FFMPEG_LOG_FILENAME_TEMP + "\" -loglevel verbose"; } /** 获取转码参数 -vcodec libx264 -profile:v baseline -preset ultrafast */ public static String getVCodecCommand() { return FFMPEG_COMMAND_VCODEC; } /** * 视频截图 * * @param videoPath 视频路径 * @param outputPath 截图输出路径 * @param wh 截图宽高,例如80x80 * @return */ public static boolean captureThumbnails(String videoPath, String outputPath, String wh) { return captureThumbnails(videoPath, outputPath, wh, "1"); } /** * 导入视频 * * @param part 分块信息 * @param mWindowWidth 窗口宽度 * @param videoWidth 视频宽度 * @param videoHeight 视频高度 * @param cropX 剪切X坐标 * @param cropY 剪切Y坐标 * @param startTime 剪切开始时间 * @param endTime 剪切介绍时间 * @param hasAudio 是否包含音频 * @return */ public static boolean importVideo(MediaPart part, int mWindowWidth, int videoWidth, int videoHeight, int cropX, int cropY, boolean hasAudio) { if (part != null && !StringUtils.isEmpty(part.tempPath)) { File f = new File(part.tempPath); if (f != null && f.exists() && !f.isDirectory()) { StringBuffer buffer = new StringBuffer("ffmpeg"); //LOG输出 buffer.append(FFMpegUtils.getLogCommand()); // 添加视频 buffer.append(" -i \""); buffer.append(part.tempPath); buffer.append("\""); //校验视频是否旋转 int rotation = -1; int width = videoWidth, height = videoHeight, cX = cropX, cY = cropY; float videoAspectRatio = videoWidth * 1.0F / videoHeight; //读取旋转信息 if (DeviceUtils.hasJellyBeanMr1()) { MediaMetadataRetriever metadata = new MediaMetadataRetriever(); metadata.setDataSource(part.tempPath); try { rotation = Integer.parseInt(metadata.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)); } catch (NumberFormatException e) { rotation = -1; } } else { rotation = UtilityAdapter.VideoGetMetadataRotate(part.tempPath); } if (rotation == 90 || rotation == 270) { int w = videoWidth; width = videoHeight; height = w; } buffer.append(" -vf \"scale="); if (width >= height) { //横向 buffer.append("-1:480"); float scaleWidth = 480 * videoAspectRatio; int viewWidth = (int) (mWindowWidth * videoAspectRatio); cX = (int) (scaleWidth * (cropX * 1.0F / viewWidth)); } else { //竖向 buffer.append("480:-1"); float scaleHeight = 480 / videoAspectRatio;//857 int viewHeight = (int) (mWindowWidth / videoAspectRatio);//964 cY = (int) (scaleHeight * (cropY * 1.0F / viewHeight));// } buffer.append("[tmp];[tmp]"); boolean hasRotation = true; switch (rotation) { case 90: buffer.append("transpose=1[transpose];[transpose]"); break; case 270: buffer.append("transpose=2[transpose];[transpose]"); break; case 180: buffer.append("vflip[vflip];[vflip]hflip[transpose];[transpose]"); break; default: hasRotation = false; break; } buffer.append(" crop=480:480:"); buffer.append(cX); buffer.append(":"); buffer.append(cY); buffer.append("\""); //去除旋转信息 if (hasRotation) { buffer.append(" -metadata:s:v rotate=\"\""); } // 裁剪时间 -ss是秒 -t也是秒 buffer.append(" -ss "); buffer.append(String.format("%.1f", part.startTime / 1000F)); buffer.append(" -t "); buffer.append(String.format("%.1f", (part.endTime - part.startTime) / 1000F)); buffer.append(" -an -vcodec rawvideo -f rawvideo -s 480x480 -pix_fmt yuv420p -r 15 \""); buffer.append(part.mediaPath); buffer.append("\""); if (!hasAudio) { //生成采样数据 // 采样率44100 * 采样位(unsigned 16bit = 2)* 声道(1) * 时间(1秒) final byte[] hz = new byte[44100 * 2 * 1 * 1]; part.prepareAudio(); try { int duration = (int) (part.endTime - part.startTime); int forCount = duration / 1000; if (forCount > 0) { for (int i = 0; i < forCount; i++) { part.mCurrentOutputAudio.write(hz); } } if (duration % 1000 != 0) { int lastSize = (int) (44100 * 2 * 1 * (duration - forCount * 1000) / 1000F); if (lastSize % 2 != 0) lastSize++; part.mCurrentOutputAudio.write(new byte[lastSize]); } } catch (IOException e) { Log.e("Yixia", "convertImage2Video", e); } catch (Exception e) { Log.e("Yixia", "convertImage2Video", e); } part.stop(); } else { // 裁剪时间 -ss是秒 -t也是秒 buffer.append(" -ss "); buffer.append(String.format("%.1f", part.startTime / 1000F)); buffer.append(" -t "); buffer.append(String.format("%.1f", (part.endTime - part.startTime) / 1000F)); buffer.append(" -vn -acodec pcm_s16le -f s16le -ar 44100 -ac 1 \""); buffer.append(part.audioPath); buffer.append("\""); } boolean result = UtilityAdapter.FFmpegRun("", buffer.toString()) == 0; if (!result) { //转码失败,写日志 VCamera.copyFFmpegLog(buffer.toString()); } return result; } } return false; } /** * 图片转视频(用于图片导入) */ public static boolean convertImage2Video(MediaPart part) { if (part != null && !StringUtils.isEmpty(part.tempPath)) { File f = new File(part.tempPath); if (f != null && f.exists() && !f.isDirectory()) { // float duration = part.duration / 1000F; // === 生成1秒的视频 === // 获取图片的宽高和方向 int width = 0, height = 0, rotation = -1, cropX = 0, cropY = 0; try { ExifInterface exif = new ExifInterface(part.tempPath); width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); rotation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); } catch (IOException e) { Log.e("Yixia", "convertImage2Video", e); } StringBuffer scaleBuffer = new StringBuffer(); if (width > 0 && height > 0) { float videoAspectRatio = width * 1.0F / height; scaleBuffer.append(" -vf \"scale="); if (width > height) { // 横向 scaleBuffer.append("-1:480"); float scaleWidth = 480 * videoAspectRatio; cropX = (int) ((scaleWidth - 480) / 2); } else { scaleBuffer.append("480:-1"); float scaleHeight = 480 / videoAspectRatio; cropY = (int) ((scaleHeight - 480) / 2); } scaleBuffer.append("[tmp];[tmp]"); // boolean hasRotation = true; switch (rotation) { case ExifInterface.ORIENTATION_ROTATE_90: scaleBuffer.append("transpose=1[transpose];[transpose]"); break; case ExifInterface.ORIENTATION_ROTATE_180: scaleBuffer.append("transpose=2[transpose];[transpose]"); break; case ExifInterface.ORIENTATION_ROTATE_270: scaleBuffer.append("vflip[vflip];[vflip]hflip[transpose];[transpose]"); break; } scaleBuffer.append(" crop=480:480:"); scaleBuffer.append(cropX); scaleBuffer.append(":"); scaleBuffer.append(cropY); scaleBuffer.append("\""); } scaleBuffer.append(" -s 480x480"); // ffmpeg -y -loop 1 -f image2 -i Goddess@2x.png -vcodec // rawvideo -r 15 -t 1 -f rawvideo -s 480x480 -pix_fmt nv21 // Goddess.yuv // -s 480x480 String cmd = String.format("ffmpeg %s -y -loop 1 -f image2 -i \"%s\" -vcodec rawvideo -r 15 -t %.1f -f rawvideo %s -pix_fmt yuv420p \"%s\"", FFMpegUtils.getLogCommand(), part.tempPath, part.duration / 1000F, scaleBuffer.toString(), part.mediaPath); if (UtilityAdapter.FFmpegRun("", cmd) == 0) { // === 生成1秒的音频 === //44100*2*1*1 // 采样率44100 * 采样位(unsigned 16bit = 2)* 声道(1) * 时间(1秒) final byte[] hz = new byte[44100 * 2 * 1 * (int) (part.duration / 1000F)]; part.prepareAudio(); try { part.mCurrentOutputAudio.write(hz); part.stop(); return true; } catch (IOException e) { Log.e("Yixia", "convertImage2Video", e); } catch (Exception e) { Log.e("Yixia", "convertImage2Video", e); } } else { //转码失败,写日志 VCamera.copyFFmpegLog(cmd); } return true; } } return false; } /** * 视频截图 * * @param videoPath 视频路径 * @param outputPath 截图输出路径 * @param wh 截图画面尺寸,例如84x84 * @param ss 截图起始时间 * @return */ public static boolean captureThumbnails(String videoPath, String outputPath, String wh, String ss) { //ffmpeg -i /storage/emulated/0/DCIM/04.04.mp4 -s 84x84 -vframes 1 /storage/emulated/0/DCIM/Camera/miaopai/1388843007381.jpg //ffmpeg -i eis-sample.mpg -s 40x40 -r 1/5 -vframes 10 %d.jpg FileUtils.deleteFile(outputPath); if (ss == null) ss = ""; else ss = " -ss " + ss; String cmd = String.format("ffmpeg -d stdout -loglevel verbose -i \"%s\"%s -s %s -vframes 1 \"%s\"", videoPath, ss, wh, outputPath); return UtilityAdapter.FFmpegRun("", cmd) == 0; } // /** // * 综合转码 // * // * @param mMediaObject 视频数据存储对象,涵主题、小部件和视频片段 // * @param targetPath 目标路径 // * @param windowWidth 视频宽度 // * @param complexWatermark 是否合并主题(预览页主题和主题音乐) // * @param needCrop 是否需要剪切(如果yuv已经是480x480就不需要裁剪了) // * @return // */ // public static boolean videoTranscoding(MediaObject mMediaObject, String targetPath, int windowWidth, boolean complexWatermark) { // if (mMediaObject == null || StringUtils.isEmpty(targetPath) || windowWidth <= 0) // return false; // // StringBuffer buffer = new StringBuffer("ffmpeg"); // // buffer.append(getLogCommand()); // // //输入音频 // buffer.append(" -f s16le -ar 44100 -ac 1 -i \""); // buffer.append(mMediaObject.getConcatPCM()); // buffer.append("\""); // // //合并主题音乐 // MediaThemeObject theme = mMediaObject.mThemeObject; // if (complexWatermark) { // if (theme != null && StringUtils.isNotEmpty(theme.audio)) { // float volume = theme.volumn; // //主题音乐 // buffer.append(" -i \""); // buffer.append(theme.audio); // buffer.append("\""); // buffer.append(" -filter_complex \""); // if (volume >= 0) { // buffer.append("[1:a]volume="); // buffer.append(String.format("%.2f", volume)); // buffer.append("[a1];[0:a][a1]"); // } // buffer.append("amix=inputs=2:duration=first:dropout_transition=2\""); // } // } // // // 添加视频 // buffer.append(" -f rawvideo -pix_fmt yuv420p -s 480x480 -r 15 -i \""); // buffer.append(mMediaObject.getConcatYUV()); // buffer.append("\""); // // //是否动态水印 // final int watermarkCount = theme == null ? 0 : theme.watermarkes.size(); // ArrayList<String> widgetsInput = new ArrayList<String>(); // ArrayList<String> widgetsOverlay = new ArrayList<String>(); // // float videoAspectRatio = windowWidth * 1.0F / 480; // // //添加水印 // if (watermarkCount > 0 && complexWatermark && theme.frameCount > 0) { // for (int i = 0; i < watermarkCount; i++) { // widgetsInput.add(String.format(" -i \"%s\"", theme.watermarkes.get(i))); // // Log.e("FFMpegUtils", "videoTranscoding...frameDuration:" + theme.frameDuration); // if (watermarkCount == 1) // widgetsOverlay.add("overlay=0:0"); // else // widgetsOverlay.add(String.format("overlay=x='if(eq(floor(mod(t*%d,%d)),%d), 0, -500)':y=0", 1000 / (theme.frameDuration / watermarkCount), watermarkCount, i)); // } // } // // //添加小部件 // for (MediaPart part : mMediaObject.getMedaParts()) { // if (part != null && part.widgets != null && part.widgets.size() > 0) { // for (int i = 0, j = part.widgets.size(); i < j; i++) { // ThemeWidget widget = part.widgets.get(i); // widgetsInput.add(String.format(" -i \"%s\"", widget.watermark)); // float minTime = (part.startTime - part.cutStartTime) / 1000F; // float maxTime = (part.endTime - (part.endTime - part.startTime - part.cutEndTime)) / 1000F; // widgetsOverlay.add(String.format("overlay=x='if(between(t, %.3f, %.3f), %d, -500)':y=%d", minTime, maxTime, (int) (widget.overlayX / videoAspectRatio), (int) (widget.overlayY / videoAspectRatio))); // } // } // } // // //讲小部件和主题加入命令行 // if (widgetsInput.size() > 0) { // buffer.append(StringUtils.join(widgetsInput, " ")); // buffer.append(" -filter_complex \""); // buffer.append(StringUtils.join(widgetsOverlay, ",")); // buffer.append("\""); // } // // buffer.append(getVCodecCommand()); // buffer.append(" -b:v " + mMediaObject.getVideoBitrate() + "k -g 30");//-ar 44100 -ac 2 -b:a 64k // buffer.append(" -acodec libfdk_aac -ar 44100 -ac 1 -b:a 64k"); // buffer.append(" -f mp4 -movflags faststart \""); // buffer.append(targetPath); // buffer.append("\""); // // boolean result = UtilityAdapter.FFmpegRun("", buffer.toString()) == 0; // if (!result) { // VCamera.copyFFmpegLog(buffer.toString()); // } // return result; // } }