Android进阶之视频压缩

文章介绍了在Android开发中使用VideoProcessor库进行视频压缩时可能出现的宽高比变形问题,提供了解决方案,包括根据原视频宽高动态设置压缩参数,以及处理压缩后文件的清理。此外,还讨论了如何避免一些异常情况,如空指针异常,确保压缩过程的稳定性。

视频压缩是一个有关视频类项目必不可少的环节,选择一个合适且稳定的压缩工具更是领开发者比较头疼的一件事情,网上压缩工具比比皆是,一旦入坑,如果出问题后期出现问题,各种成本更是令人畏惧,这篇文章或许可以让你少走一些“弯路”。
首先这里的视频压缩使用的是 VideoProcessor 介意者勿扰~,并且是音视频类实战项目长期稳定之后才写的此文章,压缩比基本保持在 7:3 左右。

接下来开始实战使用,以及遇到的问题。

1.导入依赖

com.github.yellowcath:VideoProcessor:2.4.2

2.调用方法

VideoProcessor.processor(mPresenter)
              .input(url)
              .outWidth(1600)
              .outHeight(1200)
              .output(outputUrl)
              .bitrate(mBitrate)
              .frameRate(10)
              .process()

方法介绍

.processor(mPresenter) - mPresenter 传入当前引用
.input(url) - url本地视频地址
.outWidth(1600) - 压缩后的宽度
.outHeight(1200) - 压缩后的高度
.output(outputUrl) - 压缩后的地址
.bitrate(mBitrate) - 比特率
.frameRate(10) - 帧速率

比特率会影响到压缩视频之后的效果,可以动态去设置比特率和帧速率去调整压缩效果

//默认三百万,有数据后拿数据的百分之四十
  var mBitrate = 3000000
  try {
         //拿到视频的比特率
         var media = MediaMetadataRetriever()
         media.setDataSource(locationVideoUrl)
         val extractMetadata =
               media.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
         Log.e(ContentValues.TAG, "当前视频的大小242412412 比特率->:${extractMetadata}")
         if (extractMetadata != null && extractMetadata.isNotEmpty()) {
                mBitrate = (extractMetadata.toInt() * 0.4).toInt()
          }
        } catch (e: Exception) {
           e.printStackTrace()
        }

以上就是压缩视频的使用步骤
以下是出现的问题

首先上述代码中很明显的错误就是压缩后的宽高是写死的,这样当用户传入不同形状的大小肯定会变形,所以我们可以根据原视频宽高进行压缩

基本我们会去本地拿资源会经过 onActivityResult 回调并且拿到 Intent data
可以通过

    public static List<LocalMedia> obtainMultipleResult(Intent data) {
        List<LocalMedia> result = new ArrayList<>();
        if (data != null) {
            result = (List<LocalMedia>) data.getSerializableExtra(PictureConfig.EXTRA_RESULT_SELECTION);
            if (result == null) {
                result = new ArrayList<>();
            }
            return result;
        }
        return result;
    }

LocalMedia

public class LocalMedia implements Parcelable {
    private String path;
    private String compressPath;
    private String cutPath;
    private long duration;
    private boolean isChecked;
    private boolean isCut;
    public int position;
    private int num;
    private int mimeType;
    private String pictureType;
    private boolean compressed;
    private int width;
    private int height;


    public String ossUrl;//记录上传成功后的图片地址
    public boolean isFail;//新增业务字段 是否是违规

    public LocalMedia() {

    }

    public LocalMedia(String path, long duration, int mimeType, String pictureType) {
        this.path = path;
        this.duration = duration;
        this.mimeType = mimeType;
        this.pictureType = pictureType;
    }

    public LocalMedia(String path, long duration, int mimeType, String pictureType, int width, int height) {
        this.path = path;
        this.duration = duration;
        this.mimeType = mimeType;
        this.pictureType = pictureType;
        this.width = width;
        this.height = height;
    }

    public LocalMedia(String path, long duration,
                      boolean isChecked, int position, int num, int mimeType) {
        this.path = path;
        this.duration = duration;
        this.isChecked = isChecked;
        this.position = position;
        this.num = num;
        this.mimeType = mimeType;
    }

    public String getPictureType() {
        if (TextUtils.isEmpty(pictureType)) {
            pictureType = "image/jpeg";
        }
        return pictureType;
    }

    public void setPictureType(String pictureType) {
        this.pictureType = pictureType;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getCompressPath() {
        return compressPath;
    }

    public void setCompressPath(String compressPath) {
        this.compressPath = compressPath;
    }

    public String getCutPath() {
        return cutPath;
    }

    public void setCutPath(String cutPath) {
        this.cutPath = cutPath;
    }

    public long getDuration() {
        return duration;
    }

    public void setDuration(long duration) {
        this.duration = duration;
    }


    public boolean isChecked() {
        return isChecked;
    }

    public void setChecked(boolean checked) {
        isChecked = checked;
    }

    public boolean isCut() {
        return isCut;
    }

    public void setCut(boolean cut) {
        isCut = cut;
    }

    public int getPosition() {
        return position;
    }

    public void setPosition(int position) {
        this.position = position;
    }

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public int getMimeType() {
        return mimeType;
    }

    public void setMimeType(int mimeType) {
        this.mimeType = mimeType;
    }

    public boolean isCompressed() {
        return compressed;
    }

    public void setCompressed(boolean compressed) {
        this.compressed = compressed;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.path);
        dest.writeString(this.compressPath);
        dest.writeString(this.cutPath);
        dest.writeLong(this.duration);
        dest.writeByte(this.isChecked ? (byte) 1 : (byte) 0);
        dest.writeByte(this.isCut ? (byte) 1 : (byte) 0);
        dest.writeInt(this.position);
        dest.writeInt(this.num);
        dest.writeInt(this.mimeType);
        dest.writeString(this.pictureType);
        dest.writeByte(this.compressed ? (byte) 1 : (byte) 0);
        dest.writeInt(this.width);
        dest.writeInt(this.height);
    }

    protected LocalMedia(Parcel in) {
        this.path = in.readString();
        this.compressPath = in.readString();
        this.cutPath = in.readString();
        this.duration = in.readLong();
        this.isChecked = in.readByte() != 0;
        this.isCut = in.readByte() != 0;
        this.position = in.readInt();
        this.num = in.readInt();
        this.mimeType = in.readInt();
        this.pictureType = in.readString();
        this.compressed = in.readByte() != 0;
        this.width = in.readInt();
        this.height = in.readInt();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        LocalMedia that = (LocalMedia) o;
        return path != null && path.equals(that.path);
    }

    @Override
    public int hashCode() {
        if (path != null) {
            return path.hashCode();
        } else {
            return 0;
        }

    }

    public static final Parcelable.Creator<LocalMedia> CREATOR = new Parcelable.Creator<LocalMedia>() {
        @Override
        public LocalMedia createFromParcel(Parcel source) {
            return new LocalMedia(source);
        }

        @Override
        public LocalMedia[] newArray(int size) {
            return new LocalMedia[size];
        }
    };
}

将拿到的data 转成 LocalMedia 的集合,默认取第一个 result.get(0)
此时我们就拿到了LocalMedia 这里面有我们需要的宽、高、时间、本地路径等信息
此时我们就可以将上面代码改成

Int  videoWith = 1600
Int  videoHeight = 1200
if (videoMedia.width != null && videoMedia.width != 0){
          videoWith = videoMedia.width
   }
if (videoMedia.height != null && videoMedia.height != 0){
          videoHeight = videoMedia.height
   }

VideoProcessor.processor(mPresenter)
              .input(url)
              .outWidth(videoWith )
              .outHeight(videoHeight )
              .output(outputUrl)
              .bitrate(mBitrate)
              .frameRate(10)
              .process()

以上,视频压缩之后变形的问题就解决了

我们压缩之后会出现一个新的压缩后的路径,如果不及时删除,用户手机上就会多一个压缩之后的文件,会影响用户的使用体验,当我们使用之后要及时去删除压缩后的文件(这里不贴代码了,百度一大堆,如有需要请留言~)
删除的时候,部分机型会报错,此时需要注意了!安卓现在已经不允许对 /0 文件也就是系统默认文件进行操作了,所以我们设置的压缩后的路径不要放在 /0 目录下
可以这样

//压缩视频,使用完后别忘了把压缩后的视频删除掉
                    val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
                    var outputUrl =
                        mPresenter.getExternalFilesDir("").toString() + "/Kome" + SimpleDateFormat(
                            FILENAME_FORMAT,
                            Locale.CHINA
                        ).format(System.currentTimeMillis()) + ".mp4"

这样基本都压缩步骤已经完成了,不出意外的话就要发版了,但是发上去之后就会发现部分机型会出现空指针的问题,经排查问题发生在底层源码里面 …processVideo()方法里面报错

可能最新版本已经修复此问题,如果没有可以直接重写 VideoProcessor 类,加一下防护,类似于 默认数据都是原有参数,尽量自己不要乱改

        int originWidth = 1600;
        if (retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) != null){
            originWidth = Integer.parseInt(retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH));
        }

最后贴上加防护后的VideoProcessor 代码

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioFormat;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.media.MediaMuxer;
import android.media.MediaRecorder;
import android.net.Uri;
import android.util.Pair;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

import static com.hw.videoprocessor.util.AudioUtil.getAudioBitrate;

import com.hw.videoprocessor.AudioProcessThread;
import com.hw.videoprocessor.VideoDecodeThread;
import com.hw.videoprocessor.VideoEncodeThread;
import com.hw.videoprocessor.VideoUtil;
import com.hw.videoprocessor.util.AudioFadeUtil;
import com.hw.videoprocessor.util.AudioUtil;
import com.hw.videoprocessor.util.CL;
import com.hw.videoprocessor.util.PcmToWavUtil;
import com.hw.videoprocessor.util.VideoMultiStepProgress;
import com.hw.videoprocessor.util.VideoProgressAve;
import com.hw.videoprocessor.util.VideoProgressListener;


@TargetApi(21)
public class VideoProcessor {
    final static String TAG = "VideoProcessor";
    final static String OUTPUT_MIME_TYPE = "video/avc";

    public static int DEFAULT_FRAME_RATE = 20;
    /**
     * 只有关键帧距为0的才能方便做逆序
     */
    public final static int DEFAULT_I_FRAME_INTERVAL = 1;

    public final static int DEFAULT_AAC_BITRATE = 192 * 1000;
    /**
     * 控制音频合成时,如果输入的音频文件长度不够,是否重复填充
     */
    public static boolean AUDIO_MIX_REPEAT = true;

    final static int TIMEOUT_USEC = 2500;


    public static void scaleVideo(Context context, Uri input, String output,
                                  int outWidth, int outHeight) throws Exception {
        processor(context)
                .input(input)
                .output(output)
                .outWidth(outWidth)
                .outHeight(outHeight)
                .process();
    }

    public static void cutVideo(Context context, Uri input, String output, int startTimeMs, int endTimeMs) throws Exception {
        processor(context)
                .input(input)
                .output(output)
                .startTimeMs(startTimeMs)
                .endTimeMs(endTimeMs)
                .process();
    }

    public static void changeVideoSpeed(Context context, Uri input, String output, float speed) throws Exception {
        processor(context)
                .input(input)
                .output(output)
                .speed(speed)
                .process();
    }


    /**
     * 对视频先检查,如果不是全关键帧,先处理成所有帧都是关键帧,再逆序
     */
    public static void reverseVideo(Context context, com.hw.videoprocessor.VideoProcessor.MediaSource input, String output, boolean reverseAudio, @Nullable VideoProgressListener listener) throws Exception {
        File tempFile = new File(context.getCacheDir(), System.currentTimeMillis() + ".temp");
        File temp2File = new File(context.getCacheDir(), System.currentTimeMillis() + ".temp2");
        try {
            MediaExtractor extractor = new MediaExtractor();
            input.setDataSource(extractor);
            int trackIndex = VideoUtil.selectTrack(extractor, false);
            extractor.selectTrack(trackIndex);
            int keyFrameCount = 0;
            int frameCount = 0;
   
资源下载链接为: https://pan.quark.cn/s/f7286fdf65f9 在 Android 平台上,对视频文件进行处理,尤其是压缩,是一项常见需求。这可能是因为需要减少文件大小以便分享、优化存储空间或提升应用性能。本文将基于 “Android-Video-Compressor-master” 项目,深入探讨如何在 Android 上实现视频压缩Android 中的视频压缩主要依赖于 MediaCodec API,这是一个强大的硬件加速编解码框架,支持多种视频格式。MediaExtractor 用于从源视频文件中提取媒体数据,而 MediaMuxer 则用于将编码后的数据写入新的视频文件。以下是详细过程: 使用 setDataSource() 方法指定视频源文件路径。 通过 getTrackCount() 获取轨道数量,通常视频和音频各占一轨。 调用 selectTrack() 选择要处理的视频轨道。 利用 readSampleData() 读取样本数据,为解码做准备。 创建 MediaCodec 实例,指定编码器类型,例如 “video/avc” 表示 H.264 编码。 调用 configure() 方法设置编码参数,如输出格式、分辨率等。 使用 start() 启动编码器。 从 MediaExtractor 读取原始数据,送入 MediaCodec 进行解码。 MediaCodec 的 dequeueInputBuffer() 返回输入缓冲区索引,将解码后的数据放入对应缓冲区。 调用 queueInputBuffer() 提交缓冲区,开始编码。 使用 dequeueOutputBuffer() 获取编码后的数据,包括缓冲区索引、时间戳等信息。 初始化 MediaMuxer,指定输出文件路径,设置输出格式为 MP4。 将 MediaCodec 的输出轨
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值