0°

Android通过FFmpeg实现小视频音频以及背景音乐合成

内容预览:
  • 4.修改背景音乐的声音大小~
  • 2.通过ffmpeg将视频原声和背景音乐合成为一个音频文件~
  • 3.得到最新的音频文件,通过ffmpeg命令合成到视频中~

始发于微信公众号: 程序员小乐

分享编程技能、互联网技术、生活感悟、打造干货分享平台,将总结的技术、心得、经验分享给大家,这里不只限于技术!值得你去关注,点击上方 蓝字 快速关注。本号支持 投稿



文章已经说了怎么编译出android下可用的ffmpeg so文件,并且通过传递一个字符串命令的方式实现需求,真的非常方便,这里我就用这个so来实现一个小视频简单制作功能。

目标功能:

1.分离出视频的原声。
2.得到无声的视频文件。
3.修改视频原声的声音大小。
4.修改背景音乐的声音大小。
5.视频原声和音乐合成。
6.视频和音频合成。

1.打开activity_main.xml绘制一个布局

Android通过FFmpeg实现小视频音频以及背景音乐合成

device-2017-08-02143656.png

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成


ripple_circle.xml

Android通过FFmpeg实现小视频音频以及背景音乐合成

@style/progressBarHorizontal_color

Android通过FFmpeg实现小视频音频以及背景音乐合成

progresscolorhorizontal.xml

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成



图片资源

Android通过FFmpeg实现小视频音频以及背景音乐合成

bt_start.png

Android通过FFmpeg实现小视频音频以及背景音乐合成

icon_video_ing.png

2.实现一个调用相机的类,用来录制小视频

我这里简单实现了一个工具类,主要使用Camera配合SurfaceView来录制,这些东西我就不多做解释了,直接看源码。

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

3.布局和工具类都准备好了,接下里就在我们首页MainActivity.java调用工具类录制一段最长30秒,最少8秒的视频。 AndroidManifest.xml中加入相机权限以及读写文件的权限

Android通过FFmpeg实现小视频音频以及背景音乐合成

在6.0+系统的手机上,只是在注册文件加入权限还不够的,还需要提示用户手动允许开启权限(真是麻烦)所以还需要在写一个权限管理的工具类,以及权限检测提示的activity

创建PermissionHelper.java加入内容

Android通过FFmpeg实现小视频音频以及背景音乐合成

创建PermissionsActivity.java加入以下内容(记得不要忘记在AndroidManifest.xml中注册)

/**
* 权限获取页面
* <p/>
*/

public class PermissionsActivity extends AppCompatActivity {
   //基本权限必须有
   public final static String[] PERMISSIONS = new String[]{
           Manifest.permission.WRITE_EXTERNAL_STORAGE,
           Manifest.permission.RECORD_AUDIO,
           Manifest.permission.CAMERA
   };
   public static final int PERMISSIONS_GRANTED = 1010; // 权限授权
   public static final int PERMISSIONS_DENIED = 1011; // 权限拒绝
   public static final int REQUEST_CODE = 1012; // 请求码
   private static final int PERMISSION_REQUEST_CODE = 0; // 系统权限管理页面的参数
   private static final String EXTRA_PERMISSIONS =
           "megawave.permission.extra_permission"; // 权限参数
   private static final String PACKAGE_URL_SCHEME = "package:"; // 方案
   private PermissionHelper mChecker; // 权限检测器
   private boolean isRequireCheck; // 是否需要系统权限检测, 防止和系统提示框重叠
   private static boolean isShowSetting=true;
   // 启动当前权限页面的公开接口
   public static void startActivityForResult(Activity activity, int requestCode, String... permissions) {
       startActivityForResult(activity,requestCode,true,permissions);
   }
   public static void startActivityForResult(Activity activity, int requestCode,boolean showSetting,String... permissions) {
       Intent intent = new Intent(activity, PermissionsActivity.class);
       intent.putExtra(EXTRA_PERMISSIONS, permissions);
       ActivityCompat.startActivityForResult(activity, intent, requestCode, null);
       isShowSetting = showSetting;
   }
   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       if (getIntent() == null || !getIntent().hasExtra(EXTRA_PERMISSIONS)) {
           throw new RuntimeException("PermissionsActivity需要使用静态startActivityForResult方法启动!");
       }
       setContentView(R.layout.activity_permissions);
       mChecker = new PermissionHelper(this);
       isRequireCheck = true;
   }
   @Override
   protected void onResume() {
       super.onResume();
       if (isRequireCheck) {
           String[] permissions = getPermissions();
           if (mChecker.lacksPermissions(permissions)) {
               requestPermissions(permissions); // 请求权限
           } else {
               allPermissionsGranted(); // 全部权限都已获取
           }
       } else {
           isRequireCheck = true;
       }
   }
   // 返回传递的权限参数
   private String[] getPermissions() {
       return getIntent().getStringArrayExtra(EXTRA_PERMISSIONS);
   }
   // 请求权限兼容低版本
   private void requestPermissions(String... permissions) {
       ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
   }
   // 全部权限均已获取
   private void allPermissionsGranted() {
       setResult(PERMISSIONS_GRANTED);
       finish();
   }
   /**
    * 用户权限处理,
    * 如果全部获取, 则直接过.
    * 如果权限缺失, 则提示Dialog.
    *
    * @param requestCode  请求码
    * @param permissions  权限
    * @param grantResults 结果
    */

   @Override
   public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
       if (requestCode == PERMISSION_REQUEST_CODE && hasAllPermissionsGranted(grantResults)) {
           isRequireCheck = true;
           allPermissionsGranted();
       } else {
           isRequireCheck = false;
           if(isShowSetting){
               showMissingPermissionDialog();
           }
       }
   }
   // 含有全部的权限
   private boolean hasAllPermissionsGranted(@NonNull int[] grantResults) {
       for (int grantResult : grantResults) {
           if (grantResult == PackageManager.PERMISSION_DENIED) {
               return false;
           }
       }
       return true;
   }
   // 显示缺失权限提示
   public void showMissingPermissionDialog() {
       AlertDialog.Builder builder = new AlertDialog.Builder(PermissionsActivity.this);
       builder.setTitle(R.string.label_help);
       builder.setMessage(R.string.tips_permissions);
       // 拒绝, 退出应用
       builder.setNegativeButton(R.string.label_quit, new DialogInterface.OnClickListener() {
           @Override public void onClick(DialogInterface dialog, int which) {
               setResult(-100);
               finish();
           }
       });
       builder.setPositiveButton(R.string.label_setting, new DialogInterface.OnClickListener() {
           @Override public void onClick(DialogInterface dialog, int which) {
               startAppSettings();
           }
       });
       builder.setCancelable(false);
       builder.show();
   }
   // 启动应用的设置
   private void startAppSettings() {
       Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
       intent.setData(Uri.parse(PACKAGE_URL_SCHEME + getPackageName()));
       startActivity(intent);
   }
}

回到MainActivity中,获取SurfaceView控件以及初始化MediaHelper工具类,启动相机。

@Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       requestWindowFeature(Window.FEATURE_NO_TITLE);
       WindowManager.LayoutParams p = this.getWindow().getAttributes();
       p.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;//|=:或等于,取其一
       getWindow().setAttributes(p);
       setContentView(R.layout.activity_main);
       mSurfaceView = (SurfaceView) findViewById(R.id.video_surface_view);
       mStartVideo = (ImageView) findViewById(R.id.start_video);
       mStartVideoIng = (ImageView) findViewById(R.id.start_video_ing);
       mProgress = (ProgressBar) findViewById(R.id.progress);
       mTime = (TextView) findViewById(R.id.time);
       findViewById(R.id.close).setOnClickListener(this);
       findViewById(R.id.inversion).setOnClickListener(this);
       mStartVideo.setOnClickListener(this);
       mStartVideoIng.setOnClickListener(this);
       //初始化工具类
       mMediaHelper = new MediaHelper(this);
       //设置视频存放地址的主目录
       mMediaHelper.setTargetDir(new File(new FileUtils(this).getStorageDirectory()));
       //设置录制视频的名字
       mMediaHelper.setTargetName(UUID.randomUUID() + ".mp4");
       mPermissionHelper = new PermissionHelper(this);
   }
@Override
   protected void onResume() {
       super.onResume();
       if(mPermissionHelper.lacksPermissions(PermissionsActivity.PERMISSIONS)){
           PermissionsActivity.startActivityForResult(this,PermissionsActivity.REQUEST_CODE,PermissionsActivity.PERMISSIONS);
       }else{
           //启动相机
           mMediaHelper.setSurfaceView(mSurfaceView);
       }
   }
@Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
       super.onActivityResult(requestCode, resultCode, data);
       if(resultCode == PermissionsActivity.PERMISSIONS_GRANTED){
           //启动相机
           mMediaHelper.setSurfaceView(mSurfaceView);
       }else if(resultCode == -100){
           finish();
       }
   }

FileUtils.java工具类

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成


4.相机调用成功后,点击录制按钮就开始录制视频了。


Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

到这里基本上小视频就录制成功了,点击停止录制,如果小视频时间大于8秒就跳转到下一个页面进行视频制作。(虽然只是个demo,但是细节东西还是要处理的,不然最后demo各自问题,估计要被喷一脸口水)

5.上面stopView方法逻辑中当视频录制结束后跳转到一个MakeVideoActivity中,携带了视频路径地址以及视频时长

新建MakeVideoActivity(记得在AndroidManifest.xml中注册)在这里实现我们的主要目标功能,视频音视频处理,也是本文中最重要的部分,全程使用ffmpeg来完成这些目标。

打开FFmpegRun这个类新增以下代码。

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

内容不多,主要封装给外部方便调用,加载相关的so文件。
因为我们是通过命令的方式调用ffmpeg相关的功能,执行命令是有一定的过程,所以需要在线程中来完成,然后通过一个回调的接口传递给外部,外部通过接口在回调的onEnd方法参数值result判断命令是否执行成功。
result值:0表示成功,其他失败。

6.搭建一个制作的布局提供给MakeVideoActivity

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成

Android通过FFmpeg实现小视频音频以及背景音乐合成


Android通过FFmpeg实现小视频音频以及背景音乐合成

video_seekbar.xml

Android通过FFmpeg实现小视频音频以及背景音乐合成

资源文件

Android通过FFmpeg实现小视频音频以及背景音乐合成

kaibo_icon_huakuai.png

这里先说一下实现思路(差不多是这样):

原唱:录制视频中的原声,需要通过滑动原音的seekbar来实时变化声音的大小。
伴唱:看见下面的本地音乐按钮没有?没错,就是白底黑字的那个按钮,点击它可以跳转到一个加载本地音乐的列表选择一首歌曲在返回到当前页面并且播放,通过伴唱的seekbar实时控制声音的大小。
两个声音是可以同时播放的,并且可以改变自己的音量大小不影响彼此,我这里播放音频(原声和伴唱)都是通过MediaPlayer(如果你不了解它?点我带你飞)来实现的。

所以我这里第一步就是需要把视频中的音频分割出来(采用FFmpeg分离),视频通过VideoView控件来播放,分离出来的音频用MediaPlayer控制播放。

7.因为调用ffmpeg的功能只需要我们传递一个命令等待返回就可以完成我们的需求,所以这里新建一个类(FFmpegCommands.java)来管理我们这次需要用到ffmpeg命令

打开FFmpegCommands.java加入以下两个方法:

/**
    * 提取单独的音频
    *
    * @param videoUrl
    * @param outUrl
    * @return
    */

   public static String[] extractAudio(String videoUrl, String outUrl) {
       String[] commands = new String[8];
       commands[0] = "ffmpeg";
       commands[1] = "-i";
       commands[2] = videoUrl;
       commands[3] = "-acodec";
       commands[4] = "copy";
       commands[5] = "-vn";
       commands[6] = "-y";
       commands[7] = outUrl;
       return commands;
   }

这个方法最后组成的就是一个ffmepg命令:ffmepg -i videoUrl(录制视频的文件路径) -acodec copy -vn -y outUrl(提取的音频存放路径)
简单解释一下参数含义:
-i 输入文件
-acodec 使用codec编解码
copy 拷贝原始编解码数据
-vn 不做视频记录(只提取音频)
-y 直接覆盖(如果目录下相同文件名字)
当然更多的参数学习,可通过官网查看—–>FFmpeg超级传送门
我这里主要实现本文的目标功能,更多的语法等你入门后在自己去深入了解

/**
    * 提取单独的视频,没有声音
    *
    * @param videoUrl
    * @param outUrl
    * @return
    */

   public static String[] extractVideo(String videoUrl, String outUrl) {
       String[] commands = new String[8];
       commands[0] = "ffmpeg";
       commands[1] = "-i";
       commands[2] = videoUrl;
       commands[3] = "-vcodec";
       commands[4] = "copy";
       commands[5] = "-an";
       commands[6] = "-y";
       commands[7] = outUrl;
       return commands;
   }

这两个方法就是分别得到视频文件(没有声音),音频文件(视频的原声来这)

回到MakeVideoActivity中开始使调用ffmpeg
初始化布局:

@Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_make_video);
       mVideoView = (VideoView) findViewById(R.id.video);
       mAudioSeekBar = (AppCompatSeekBar) findViewById(R.id.video_seek_bar);
       mMusicSeekBar = (AppCompatSeekBar) findViewById(R.id.music_seek_bar);
       mAudioSeekBar.setOnSeekBarChangeListener(this);
       mMusicSeekBar.setOnSeekBarChangeListener(this);
       findViewById(R.id.next).setOnClickListener(this);
       findViewById(R.id.back).setOnClickListener(this);
       findViewById(R.id.local_music).setOnClickListener(this);
       isPlayer = getIntent().getBooleanExtra(this.getClass().getSimpleName(), false);
       if (isPlayer) {
           findViewById(R.id.title_layout).setVisibility(View.GONE);
           findViewById(R.id.editor_layout).setVisibility(View.GONE);
             mVideoView.setVideoPath(getIntent().getStringExtra("path"));
           mVideoView.start();
       }else{
           mFileUtils = new FileUtils(this);
           mTargetPath = mFileUtils.getStorageDirectory();
           extractVideo();
       }
   }

其他操作按钮没什么解释,其中有个参数isPlayer,主要最后点击下一步生成文件,我又会Intent到当前MakeVideoActivity中,这时候isPlayer=true,就是播放最后制作完成的成品视频。(偷个懒,少创建一个文件)

新增以下方法

/**
    * 提取视频
    */

   private void extractVideo() {
       final String outVideo = mTargetPath + "/video.mp4";
       String[] commands = FFmpegCommands.extractVideo(getIntent().getStringExtra("path"), outVideo);
       FFmpegRun.execute(commands, new FFmpegRun.FFmpegRunListener() {
           @Override
           public void onStart() {
               mMediaPath = new ArrayList<>();
               Log.e(TAG,"extractVideo ffmpeg start...");
           }
           @Override
           public void onEnd(int result) {
               Log.e(TAG,"extractVideo ffmpeg end...");
               mMediaPath.add(outVideo);
               extractAudio();
           }
       });
   }
/**
    * 提取音频
    */

   private void extractAudio() {
       final String outVideo = mTargetPath + "/audio.aac";
       String[] commands = FFmpegCommands.extractAudio(getIntent().getStringExtra("path"), outVideo);
       FFmpegRun.execute(commands, new FFmpegRun.FFmpegRunListener() {
           @Override
           public void onStart()
{
               mAudioPlayer = new MediaPlayer();
           }
           @Override
           public void onEnd(int result)
{
               Log.e(TAG,"extractAudio ffmpeg end...");
               mMediaPath.add(outVideo);
               String path = mMediaPath.get(0);
               mVideoView.setVideoPath(path);
               try {
                   mAudioPlayer.setDataSource(mMediaPath.get(1));
                   mAudioPlayer.setLooping(true);
                   mAudioPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                       @Override
                       public void onPrepared(MediaPlayer mediaPlayer)
{
                           mAudioPlayer.setVolume(0.5f, 0.5f);
                           mAudioPlayer.start();
                       }
                   });
                   mAudioPlayer.prepare();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       });
   }

在onCreate中调用extractVideo方法开始执行FFmpeg命令,如果不出现意执行完命令后,音频文件通过MediaPlayer开始播放,视频文件通过VideoView加载,音频默认50%音量。
实现seekbar进度条变化监听实现:

@Override
   public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
       float volume = i / 100f;
       if (mAudioSeekBar == seekBar) {
           mAudioPlayer.setVolume(volume, volume);
       } else if(mMusicPlayer!=null){
           mMusicPlayer.setVolume(volume, volume);
       }
   }

这里的视频和音频已经被单独分开,大概是这样(gif质量较差,勉强看哈这个意思就行,声音也无法听见,很尴尬。)

Android通过FFmpeg实现小视频音频以及背景音乐合成

video_ffmpeg.gif

其实能够实现这一步说明我们编译的ffmpeg so文件是没有任何问题的,可以传递更多的命令来完成更多的功能,所以继续下一步,选择一首音乐合成到视频中,最后视频有原声也有音乐,并且两种声音控制在最后分别选择的音量进度上。

8.选择本地音乐,并且剪切音频文件和视频时长一样。

新建一个MusicActivity用来展示本地音乐列表

public class MusicActivity extends AppCompatActivity {
   private ListView mListView;
   private MusicAdapter mAdapter;
   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_music);
       mListView = (ListView) findViewById(R.id.list);
       findViewById(R.id.back).setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View view) {
               finish();
           }
       });
       new SongTask().execute();
   }
   private class SongTask extends AsyncTask<Void, Void, List<Music>> implements AdapterView.OnItemClickListener{
       @Override
       protected void onPreExecute() {
           super.onPreExecute();
       }
       @Override
       protected List<Music> doInBackground(Void... voids) {
           List<Music> musics = new ArrayList<>();
           Cursor cursor = getApplicationContext().getContentResolver().query(
                   MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null,
                   MediaStore.Audio.Media.DATA + " like ?",
                   new String[]{Environment.getExternalStorageDirectory() + File.separator + "%"},
                   MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
           if (cursor != null) {
               for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
                   Music music = new Music();
                   String isMusic = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_MUSIC));
                   if (isMusic != null && isMusic.equals("")) continue;
//                    int duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
                   String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
                   Log.e("SLog","music:"+path);
                   if (!path.endsWith(".mp3")) {
                       continue;
                   }
                   String title = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
                   String artist = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
                   music.setId(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)));
                   music.setName(title);
                   music.setSingerName(artist);
                   music.setSongUrl(path);
                   musics.add(music);
               }
               cursor.close();
           }
           return musics;
       }
       @Override
       protected void onPostExecute(List<Music> musics) {
           super.onPostExecute(musics);
           mAdapter = new MusicAdapter(MusicActivity.this,musics);
           mListView.setAdapter(mAdapter);
           mListView.setOnItemClickListener(this);
       }
       @Override
       public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
           Music music = mAdapter.getItem(i);
           Intent intent = new Intent();
           intent.putExtra("music",music.getSongUrl());
           setResult(10000,intent);
           finish();
       }
   }
}

这里面有个内部类SongTask,防止主UI被卡死,所以在线程中完成本地音乐的遍历,可以合成的音乐格式有很多种,但是可能需要做不同的格式处理,我这里暂时只获取了MP3格式的音频文件。

适配器比较简单,这里就不贴了,后面上传的源码会携带。

Android通过FFmpeg实现小视频音频以及背景音乐合成

9.视频不足30秒,一般的mp3音乐都在几分钟,所以现在点击一个音乐,返回到制作页面MakeVideoActivity需要通过ffmpeg命令剪切音乐后再进行播放。

打开FFmpegCommands新增剪切的命令。

/**
    * 裁剪音频
    */

   public static String[] cutIntoMusic(String musicUrl, long second, String outUrl) {
       String[] commands = new String[10];
       commands[0] = "ffmpeg";
       commands[1] = "-i";
       commands[2] = musicUrl;
       commands[3] = "-ss";
       commands[4] = "00:00:10";
       commands[5] = "-t";
       commands[6] = String.valueOf(second);
       commands[7] = "-acodec";
       commands[8] = "copy";
       commands[9] = outUrl;
       return commands;
   }

参数解释:

-ss 音频开始位置,我这里是从低10秒开始
-t 音频结束位置,我这里传递了一个参数过来,就是视频的时长,
最后得到的音频就是这个mp3文件第10秒开始到10+ second秒结束。
我这里处理完的音频时长和视频时长保持一样。

新增一个方法

private void cutSelectMusic(String musicUrl) {
       final String musicPath = mTargetPath + "/bgMusic.aac";
       long time = getIntent().getIntExtra("time",0);
       String[] commands = FFmpegCommands.cutIntoMusic(musicUrl, time, musicPath);
       FFmpegRun.execute(commands, new FFmpegRun.FFmpegRunListener() {
           @Override
           public void onStart() {
                   Log.e(TAG,"cutSelectMusic ffmpeg start...");
           }
           @Override
           public void onEnd(int result) {
               Log.e(TAG,"cutSelectMusic ffmpeg end...");
               if(mMusicPlayer!=null){//移除上一个选择的音乐背景
                   mMediaPath.remove(mMediaPath.size()-1);
               }
               mMediaPath.add(musicPath);
               stopMediaPlayer();
               mMusicPlayer = new MediaPlayer();
               try {
                   mMusicPlayer.setDataSource(musicPath);
                   mMusicPlayer.setLooping(true);
                   mMusicPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
                       @Override
                       public void onPrepared(MediaPlayer mediaPlayer) {
                           mediaPlayer.setVolume(0.5f, 0.5f);
                           mediaPlayer.start();
                           mMusicSeekBar.setProgress(50);
                       }
                   });
                   mMusicPlayer.prepareAsync();
               } catch (IOException e) {
                   e.printStackTrace();
               }
           }
       });
   }

调用

@Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
       super.onActivityResult(requestCode, resultCode, data);
       if (resultCode == 10000) {
           String music = data.getStringExtra("music");
           cutSelectMusic(music);
       }
   }

现在制作页面就使用了三个独立文件,一个播放的视频(没有原声),一个视频的原声,一个刚才选择的背景音乐。到这里基本上准备的东西都已经完成了,调节好需要的音量,点击下一步就开始合成。

10.作为刚入门的时候真的苦不堪言,简直有种从入门到放弃的感觉,视频合成多音频我到现在也没弄明白,总之走了很多弯路,各自尝试失败,要么就是达不到要求,其中的弯路过程我就不在这里阐述了,我怕说多了刹不住车把你们带沟里,直接给出最后音视频+背景音乐合成成功的思路:

1.根据两个seebar选择的音量进度,通过ffmpeg命令修改视频原声和背景音乐的对应音量。
2.通过ffmpeg将视频原声和背景音乐合成为一个音频文件。
3.得到最新的音频文件,通过ffmpeg命令合成到视频中。

打开FFmpegCommands新增三个方法。

/**
    * 修改音频文件的音量
    * @param audioOrMusicUrl
    * @param vol
    * @param outUrl
    * @return
    */

   public static String[] changeAudioOrMusicVol(String audioOrMusicUrl, int vol, String outUrl) {
       if (SLog.debug)
           SLog.w("audioOrMusicUrl:" + audioOrMusicUrl + "nvol:" + vol + "noutUrl:" + outUrl);
       String[] commands = new String[8];
       commands[0] = "ffmpeg";
       commands[1] = "-i";
       commands[2] = audioOrMusicUrl;
       commands[3] = "-vol";
       commands[4] = String.valueOf(vol);
       commands[5] = "-acodec";
       commands[6] = "copy";
       commands[7] = outUrl;
       return commands;
   }

-vol 就是主要改变音量的值。

/**
    * @param audio1
    * @param audio2
    * @param outputUrl
    * @return
    */

   public static String[] composeAudio(String audio1, String audio2, String outputUrl) {
       Log.w("SLog","audio1:" + audio1 + "naudio2:" + audio2 + "noutputUrl:" + outputUrl);
       String[] commands = new String[10];
       commands[0] = "ffmpeg";
       //输入
       commands[1] = "-i";
       commands[2] = audio1;
       //音乐
       commands[3] = "-i";
       commands[4] = audio2;
       //覆盖输出
       commands[5] = "-filter_complex";
       commands[6] = "amix=inputs=2:duration=first:dropout_transition=2";
       commands[7] = "-strict";
       commands[8] = "-2";
       //输出文件
       commands[9] = outputUrl;
       return commands;
   }

混合合并两个音频文件
-filter_complex 很强大,很多滤镜功能,更多的效果可通过官网学习。
我这里用它指定2个音频文件合成,最后的合成的音频时长以第一个音频的时长为准。

/**
    * 音频,视频合成
    * @param videoUrl
    * @param musicOrAudio
    * @param outputUrl
    * @param second
    * @return
    */

   public static String[] composeVideo(String videoUrl, String musicOrAudio, String outputUrl, long second) {
       Log.w("SLog","videoUrl:" + videoUrl + "nmusicOrAudio:" + musicOrAudio + "noutputUrl:" + outputUrl + "nsecond:" + second);
       String[] commands = new String[14];
       commands[0] = "ffmpeg";
       //输入
       commands[1] = "-i";
       commands[2] = videoUrl;
       //音乐
       commands[3] = "-i";
       commands[4] = musicOrAudio;
       commands[5] = "-ss";
       commands[6] = "00:00:00";
       commands[7] = "-t";
       commands[8] = String.valueOf(second);
       //覆盖输出
       commands[9] = "-vcodec";
       commands[10] = "copy";
       commands[11] = "-acodec";
       commands[12] = "copy";
       //输出文件
       commands[13] = outputUrl;
       return commands;
   }

把音频文件合成到视频。
回到MakeVideoActivity点击下一步开始制作视频
修改onClick增加下一步的点击事件

@Override
   public void onClick(View view) {
       switch (view.getId()){
           case R.id.back:
               finish();
               mFileUtils.deleteFile(mTargetPath,null);
               break;
           case R.id.local_music:
               Intent intent = new Intent(this,MusicActivity.class);
               startActivityForResult(intent,0);
               break;
           case R.id.next:
               composeVideoAudio();
               mNext.setTextColor(Color.parseColor("#999999"));
               mNext.setEnabled(false);
               break;
       }
   }
/**
    * 处理视频原声
    */

   private void composeVideoAudio() {
       int mAudioVol = mAudioSeekBar.getProgress();
       String audioUrl = mMediaPath.get(1);
       final String audioOutUrl = mTargetPath + "/tempAudio.aac";
       String[] common = FFmpegCommands.changeAudioOrMusicVol(audioUrl, mAudioVol * 10, audioOutUrl);
       FFmpegRun.execute(common, new FFmpegRun.FFmpegRunListener() {
           @Override
           public void onStart() {
               Log.e(TAG,"changeAudioVol ffmpeg start...");
               handler.sendEmptyMessage(0);
           }
           @Override
           public void onEnd(int result) {
               Log.e(TAG,"changeAudioVol ffmpeg end...");
               if (mMediaPath.size() == 3) {
                   composeVideoMusic(audioOutUrl);
               } else {
                   composeMusicAndAudio(audioOutUrl);
               }
           }
       });
   }

在onEnd方法有个判断逻辑,如果没有选择本地音乐,那不需要合成背景音乐,只需要处理视频原声后直接合成到视频完成制作,否则就继续处理当前选择的背景音乐。

 /**
    * 处理背景音乐
    */

   private void composeVideoMusic(final String audioUrl) {
       final int mMusicVol = mMusicSeekBar.getProgress();
       String musicUrl;
       if (audioUrl == null) {
           musicUrl = mMediaPath.get(1);
       } else {
           musicUrl = mMediaPath.get(2);
       }
       final String musicOutUrl = mTargetPath + "/tempMusic.aac";
       final String[] common = FFmpegCommands.changeAudioOrMusicVol(musicUrl, mMusicVol * 10, musicOutUrl);
       FFmpegRun.execute(common, new FFmpegRun.FFmpegRunListener() {
           @Override
           public void onStart() {
               Log.e(TAG,"changeMusicVol ffmpeg start...");
               handler.sendEmptyMessage(0);
           }
           @Override
           public void onEnd(int result) {
               Log.e(TAG,"changeMusicVol ffmpeg end...");
               composeAudioAndMusic(audioUrl, musicOutUrl);
           }
       });
   }

原声和背景音乐都处理好了就把两个音频合成一个

/**
    * 合成原声和背景音乐
    */

   public void composeAudioAndMusic(String audioUrl, String musicUrl) {
       if (audioUrl == null) {
           composeMusicAndAudio(musicUrl);
       } else {
           final String musicAudioPath = mTargetPath + "/audioMusic.aac";
           String[] common = FFmpegCommands.composeAudio(audioUrl, musicUrl, musicAudioPath);
           FFmpegRun.execute(common, new FFmpegRun.FFmpegRunListener() {
               @Override
               public void onStart() {
                   Log.e(TAG,"composeAudioAndMusic ffmpeg start...");
                   handler.sendEmptyMessage(0);
               }
               @Override
               public void onEnd(int result) {
                   Log.e(TAG,"composeAudioAndMusic ffmpeg end...");
                   composeMusicAndAudio(musicAudioPath);
               }
           });
       }
   }

得到最后的音频文件合成到无声的视频中

/**
    * 视频和背景音乐合成
    *
    * @param bgMusicAndAudio
    */

   private void composeMusicAndAudio(String bgMusicAndAudio) {
       final String videoAudioPath = mTargetPath + "/videoMusicAudio.mp4";
       final String videoUrl = mMediaPath.get(0);
       final int time = getIntent().getIntExtra("time",0) - 1;
       String[] common = FFmpegCommands.composeVideo(videoUrl, bgMusicAndAudio, videoAudioPath, time);
       FFmpegRun.execute(common, new FFmpegRun.FFmpegRunListener() {
           @Override
           public void onStart() {
               Log.e(TAG,"videoAndAudio ffmpeg start...");
               handler.sendEmptyMessage(0);
           }
           @Override
           public void onEnd(int result) {
               Log.e(TAG,"videoAndAudio ffmpeg end...");
               handleVideoNext(videoAudioPath);
           }
       });
   }

如果不出意外就会的到最后制作成功的视频文件,进行其他逻辑处理。

/**
    * 适配处理完成,进入下一步
    */

   private void handleVideoNext(String videoUrl) {
       Message message = new Message();
       message.what = 1;
       message.obj = videoUrl;
       handler.sendMessage(message);
   }
   Handler handler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           super.handleMessage(msg);
           switch (msg.what) {
               case 0:
                   showProgressLoading();
                   break;
               case 1:
                   dismissProgress();
                   String videoPath = (String) msg.obj;
                   Intent intent = new Intent(MakeVideoActivity.this,MakeVideoActivity.class);
                   intent.putExtra("path",videoPath);
                   intent.putExtra(this.getClass().getSimpleName(),true);
                   startActivity(intent);
                   finish();
                   break;
               case 2:
                   dismissProgress();
                   break;
           }
       }
   };
   private void showProgressLoading(){
   }
   private void dismissProgress(){
   }

我这里就是跳转到到当前页面直接播放。

到这里我们就实现了所有的目标功能,当然这里我所用到的ffmpeg功能只是冰山一角,它的更多功能需要自己去挖掘,希望我这里的两篇文章能够将你引入android ffmpeg的入门。
后面有时间我将会提供一个android opencv的demo的功能实现。

源码地址

https://github.com/tangyxgit/FFmpegVideo

读书不一定改变命运,学习一定让你进步。

Android通过FFmpeg实现小视频音频以及背景音乐合成


如何您想进技术群和大牛们交流,关注公众号在后台回复 “加群”,或者 “学习” 即可

作者:Galaxy北爱

链接:http://www.jianshu.com/p/7156d1007f84

本文来自Galaxy北爱投稿

如果您觉得不错,请别忘了分享到您的朋友圈让更多的人看到!! 您的举手之劳,就是对我最好的支持,非常感谢!

每日英文


Life is too short to wake up in the morning with regrets. So, love the people who treat you right and forget about the ones who do not.

生命太短,没留时间给我们每日带着遗憾醒来。所以去爱那些对你好的人,忘掉那些不知珍惜你的人。


乐乐有话说


把懒惰放一边,把丧气的话收一收,把积极性提一提,把矫情的心放一放,所有想要的,都得靠自己的努力才能得到。


Android通过FFmpeg实现小视频音频以及背景音乐合成


推荐阅读



看完本文有收获?请转发分享给更多人
关注「杨守乐」,提升编程技能

以上就是:Android通过FFmpeg实现小视频音频以及背景音乐合成 的全部内容。

本站部分内容来源于互联网和用户投稿,如有侵权请联系我们删除,谢谢。
Email:[email protected]


0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论