利用文件随机读取类RandomAccessFile做断点重读

  模拟通过文件读写方式对音视频流进行格式转换后,获取音频内容,并支持读取中断后再起线程读取,断开重读的关键在于随机读取文件,它支持保持一个类似游标的文件字节索引,只要每次读取都能把这个游标记录在内存中,那么就算当前的线程意外死掉,新的线程也能直接拿到游标值,按最后一次读取的位置重新开始读。直接看代码:

import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;

import java.io.*;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.concurrent.TimeUnit;

/**
 * 一个线程把音视频转单声道16位16K赫兹小端点pcm音频,另一个线程读取后意外中断,再起一个线程接着读
 */
public class PCMFileProduceAndConsumer {
    static boolean isGrabeFinish = false;
    static boolean isFirstCome = true;
    static boolean isFirstThread = true;
    static int someOffset = 33333333;
    static final int interval = 500; // 间隔时间,单位毫秒
    static long offset = 0; // 随机读取的下标位置,从头开始读
    static String sourcePath = "E:\\BaiduNetdiskDownload\\roundDeskS02E01.mp4"; //待转音视频
    static String targetPath = "E:\\BaiduNetdiskDownload\\tmp.pcm"; //目标文件

    public static void main(String[] args) throws Exception {
        // 拉起一个线程把MP4转pcm音频文件
        new Thread(new FileInput()).start();

        // 先等两秒,让音频数据写进去一些
        TimeUnit.SECONDS.sleep(1);

        // 拉起另一个线程读取音频文件,支持断连后再重连读取
        new Thread(new FileOutput()).start();

        // 上面的线程中断后拉起一个新的线程,等待上个线程死掉
        while (offset < someOffset) {
            Thread.sleep(20);
        }
        new Thread(new FileOutput()).start();
    }

    /**
     * 文件写入
     */
    static class FileInput implements Runnable {

        @Override
        public void run() {
            try {
                OutputStream os = new BufferedOutputStream(new FileOutputStream(targetPath));
                FFmpegFrameGrabber frameGrabber = FFmpegFrameGrabber.createDefault(sourcePath);
                frameGrabber.setSampleRate(16000); //16K赫兹采样率
                frameGrabber.setAudioChannels(1); //单声道
                frameGrabber.start();

                Frame frame;
                Buffer buffer;
                short[] shorts;
                byte[] bytes;

                System.out.println("开始写入");
                while ((frame = frameGrabber.grabSamples()) != null) {
                    if (frame.samples == null) {
                        continue;
                    }
                    buffer = frame.samples[0];

                    shorts = new short[buffer.limit()];
                    ((ShortBuffer) buffer).get(shorts);
                    bytes = shortArr2byteArr(shorts, buffer.limit());

                    os.write(bytes);
                }

                os.close(); // 关闭写文件
                frameGrabber.close(); // 直接关闭拉流
                isGrabeFinish = true;
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("写入结束");
        }
    }

    /**
     * 文件读出
     */
    static class FileOutput implements Runnable {
        long timestart;

        @Override
        public void run() {
            byte[] buf = new byte[1280];
            System.out.println("开始读取.最开始的位置:" + offset);
            try (RandomAccessFile raf = new RandomAccessFile(targetPath, "r")) {
                int len;
                raf.seek(offset); // 定位到指定下标位置

                // 当文件在写入时,尚未读取到文件内容时,等等写入线程
                while ((len = raf.read(buf)) != -1 || !isGrabeFinish) {

                    // 有可能读取速度快于写入速度,空转CPU等待
                    if (len == -1) {
                        Thread.sleep(10);
                        continue;
                    }

                    // 获取每10秒的第一次开始时间
                    if (isFirstCome) {
                        timestart = System.currentTimeMillis();
                        isFirstCome = false;
                    }

                    if (System.currentTimeMillis() - timestart > interval) {
                        System.out.println("当前读取位置: " + offset);
                        isFirstCome = true;
                    }

                    // 模拟线程突然被中断,拉起另一个线程继续读取
                    if (isFirstThread && offset >= someOffset) {
                        isFirstThread = false;
                        break;
                    }

                    offset += len;
                    raf.seek(offset); // 定位到下次准备读取位置
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println("读取结束. 最后的位置: " + offset);
        }
    }


    /**
     * 8位字节数组转16位字节数组,也就是16位的采样位深,转小端点字节数组
     *
     * @param shortArr
     * @param shortArrLen
     * @return
     */
    private static byte[] shortArr2byteArr(short[] shortArr, int shortArrLen) {
        byte[] byteArr = new byte[shortArrLen * 2];
        ByteBuffer.wrap(byteArr).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(shortArr);
        return byteArr;
    }

}

 

  运行结果:

开始读取.最开始的位置:0
开始写入
当前读取位置: 956258
当前读取位置: 2296700
当前读取位置: 4213894
当前读取位置: 7486824
当前读取位置: 9374884
当前读取位置: 12292798
当前读取位置: 14785692
当前读取位置: 18087756
当前读取位置: 20425354
当前读取位置: 23694724
当前读取位置: 27250164
当前读取位置: 32015270
读取结束. 最后的位置: 33333750
开始读取.最开始的位置:33333750
当前读取位置: 33333750
当前读取位置: 37663106
当前读取位置: 43335462
当前读取位置: 47724590
当前读取位置: 53208958
当前读取位置: 60212088
当前读取位置: 65910460
当前读取位置: 72629014
写入结束
读取结束. 最后的位置: 76616902
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'E:\BaiduNetdiskDownload\roundDeskS02E01.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.20.100
  Duration: 00:39:54.27, start: 0.000000, bitrate: 815 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1080x606, 744 kb/s, 25 fps, 25 tbr, 90k tbn, 50 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 64 kb/s (default)
    Metadata:
      handler_name    : SoundHandler

Process finished with exit code 0

 

posted on 2020-08-09 16:46  不想下火车的人  阅读(295)  评论(0编辑  收藏  举报

导航