Java怎么实现提取QSV文件视频内容

寻技术 JAVA编程 2023年07月12日 113

今天小编给大家分享一下Java怎么实现提取QSV文件视频内容的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。

创建类

第一步新建一个java类

QSV
,构造函数传入需要解析的文件名称。
public class QSV {
    
    private RandomAccessFile randomAccessFile;
    private String name;

    public QSV(String name) throws FileNotFoundException {
        randomAccessFile = new RandomAccessFile(name, "r");
        this.name = name;
    }

}

通用方法

逐字节读取文件

我们需要逐个字节读取文件,这边就需要jdk自带的类

RandomAccessFile
,先定义一个通用函数,输出偏移位置和字节大小获取字节数组。
/**
* @param offset 偏移未知
* @param size   字节大小
* @return 字节数组
* @throws IOException
*/
private byte[] getByteFromFile(int offset, int size) throws IOException {
    //指针移动至偏移位置
    randomAccessFile.seek(offset);
    //需要读取的字节数
    byte[] bs = new byte[size];
    randomAccessFile.read(bs);
    return bs;
}

字节数组转16进制字符串

private static final char[] HEX_VOCABLE = {'0', '1', '2', '3', '4', '5', '6', '7',
        '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
/**
* 字节数组转16进制字符串
*
* @param bs
* @return
*/
public String bytesToHex(byte[] bs) {
    StringBuilder sb = new StringBuilder();
    for (byte b : bs) {
        int high = (b >> 4) & 0x0f;
        int low = b & 0x0f;
        sb.append(HEX_VOCABLE[high]);
        sb.append(HEX_VOCABLE[low]);
    }
    return sb.toString();
}

字节数组转int

/**
* 字节数组转int
*
* @param b
* @return
*/
public int toInt(byte[] b) {
    int res = 0;
    for (int i = 0; i < b.length; i++) {
        res += (b[i] & 0xff) << (i * 8);
    }
    return res;
}

老版本解密算法

老版本解密方法比较简单,先看下C的写法:

// 定义1个字节内存
typedef uint8_t BYTE;
// 定义4个字节内存
typedef uint32_t DWORD;
// decryption algorithm for some segments in qsv version 0x1
void decrypt_1(BYTE* buffer, DWORD size) {
    static BYTE dict[] = {0x62, 0x67, 0x70, 0x79};
    for(int i = 0; i < size; ++i) {
        DWORD j = ~i & 0x3;
        buffer[i] ^= dict[j];
    }
}

可以直接用byte数组和long类型替换c的内存定义,这边没问题时因为没有位移算法,后面新版本解密算法会讲到。

private void decrypt_1(byte[] bs, long len) {
    byte[] b = new byte[]{0x62, 0x67, 0x70, 0x79};
    for (int i = 0; i < len; i++) {
        //先取反再与运算
        int j = ~i & 0x3;
        //异或运算
        bs[i] ^= b[j];
    }
}

新版本解密算法

新版本解密算法比较复杂,同样先看下c的写法:

// 定义1个字节内存
typedef uint8_t BYTE;
// 定义4个字节内存
typedef uint32_t DWORD;
// decryption algorithm for some segments in qsv version 0x2
void decrypt_2(BYTE* buffer, DWORD size) {
    DWORD x = 0x62677079;
    for(DWORD i = size - 1; i != 0; --i) {
        x = (x << 1) | (x >> 31);
        x ^= buffer[i];
    }
    for(DWORD i = 1; i < size; ++i) {
        x ^= buffer[i] & 0xFF;
        x = (x >> 1) | (x << 31);
        DWORD j = x % i;
        BYTE tmp = buffer[j];
        buffer[j] = tmp ^ (BYTE)~buffer[i];
        buffer[i] = tmp;
    }
}

这边注意c的写法有位移,定义的

DWORD
占32位内存,而java与之对应的
int
类型也是占32位内存,取值范围是
-2^31~2^31-1
(即
-2147483648~2147483647
),如果超出这个范围java需要用long,而long占64位内存取反和位移都是按64位进行的与C的32位取反位移计算结果肯定有差异。

比如:2147483648,转换位二进制表示:10000000000000000000000000000000,C的写法由于

DWORD
占32位内存,左移一位后超出32为故就变成00000000000000000000000000000000,而java已经超出了int的取值范围,只能用long定义,由于long占64位,左移一位不会超出,结果也就不一样了。

又比如:9147483648,转为二进制表示:1000100001001110111000011000000000,已经超出32位,存到

DWORD
中的只保留右边的32位,即00100001001110111000011000000000,左移以为变成01000010011101110000110000000000,转为10进制为1115098112,而java采用long定义不会超出64位,左移结果位10001000010011101110000110000000000,转为十进制表示为18294967296,与C的结果相差较大。那么有办法解决吗,答案肯定是有的,我们需要重新写一个位移算法。
/**
* 左移
*
* @param value long
* @param i     位移量
* @return long
*/
private long longLeftShift(long value, int i) {
    String binary = format32(value);
    binary = binary.substring(i) + String.format("%0" + i + "d", 0);
    return Long.parseLong(binary, 2);
}

/**
* 右移
*
* @param value long
* @param i     位移量
* @return long
*/
private long longRightShift(long value, int i) {
    String binary = format32(value);
    binary = String.format("%0" + i + "d", 0) + binary.substring(0, binary.length() - i);
    return Long.parseLong(binary, 2);
}

private String format32(long value) {
    String binary = Long.toBinaryString(value);
    if (binary.length() < 32) {
        //补满32位
        binary = String.format("%0" + (32 - binary.length()) + "d", 0) + binary;
    } else {
        //多余的截取掉
        binary = binary.substring(binary.length() - 32);
    }
    return binary;
}

有了新定义的位移算法,解密算法就可以修改为:

private void decrypt_2(byte[] buffer, int size) {
    long x = 0x62677079;
    for (int i = size - 1; i != 0; --i) {
        x = longLeftShift(x, 1) | longRightShift(x, 31);
        x ^= buffer[i] & 0xFF;
    }
    for (int i = 1; i < size; ++i) {
        x ^= buffer[i] & 0xFF;
        x = longRightShift(x, 1) | longLeftShift(x, 31);
        int j = (int) (x % i);
        byte tmp = buffer[j];
        int a = buffer[i];
        buffer[j] = (byte) (tmp ^ ~a);
        buffer[i] = tmp;
    }
}

获取QSV头部信息

上一篇文章讲到,头部信息共90个字节,

signature
(10byte)+
version
(4byte)+
vid
(16byte)+
_unknown1
(4byte)+
_unknown2
(32byte)+
_unknown3
(4byte)+
_unknown4
(4byte)+
json_offset
(8byte)+
json_size
(4byte)+
nb_indices
(4byte)
public static void main(String[] args) throws IOException {
    QSV qsv = new QSV("D:workspacec风吹半夏第1集-蓝光1080P.qsv");
    qsv.readHead();
    qsv.close();
}

public void readHead() throws IOException {
    //signature:10byte
    byte[] signature = getByteFromFile(0x0, 0xA);
    //version:4byte
    byte[] version = getByteFromFile(0xA, 0x4);
    //vid:16byte
    byte[] vid = getByteFromFile(0xE, 0x10);
    //_unknown1:4byte
    byte[] _unknown1 = getByteFromFile(0x1E, 0x4);
    //_unknown2:32byte
    byte[] _unknown2 = getByteFromFile(0x22, 0x20);
    //_unknown3:4byte
    byte[] _unknown3 = getByteFromFile(0x42, 0x4);
    //_unknown4:4byte
    byte[] _unknown4 = getByteFromFile(0x46, 0x4);
    //json_offset:8byte
    byte[] json_offset = getByteFromFile(0x4A, 0x8);
    //json_size:4byte
    byte[] json_size = getByteFromFile(0x52, 0x4);
    //nb_indices:4byte
    byte[] nb_indices = getByteFromFile(0x56, 0x4);
    System.out.println(String.format("signature:%s
version:%s
vid:%s
_unknown1:%s
_unknown2:%s
_unknown3:%s
_unknown4:%s
json_offset:0x%s
json_size:0x%s
nb_indices:0x%s",
            new String(signature), toInt(version), bytesToHex(vid), bytesToHex(_unknown1), bytesToHex(_unknown2), bytesToHex(_unknown3),
            bytesToHex(_unknown4), Integer.toHexString(toInt(json_offset)), Integer.toHexString(toInt(json_size)), Integer.toHexString(toInt(nb_indices))));
}

运行结果:

signature:QIYI VIDEO
version:2
vid:A90EA47D4333331BB85F331C1130504C
_unknown1:01000000
_unknown2:0000000000000000000000000000000000000000000000000000000000000000
_unknown3:01000000
_unknown4:01000000
json_offset:0x167
json_size:0x1d81
nb_indices:0x7

获取json数据

获取json数据需要先获取头部信息记录的json的偏移位置和字节大小,获取到的字节数组再通过老版本解密算法解密即可。

public static void main(String[] args) throws IOException {
    QSV qsv = new QSV("D:workspacec风吹半夏第1集-蓝光1080P.qsv");
    qsv.readJson();
    qsv.close();
}

public void readJson() throws IOException {
    //json_offset:8byte
    byte[] json_offset = getByteFromFile(0x4A, 0x8);
    //json_size:4byte
    byte[] json_size = getByteFromFile(0x52, 0x4);
    //json
    byte[] bs = getByteFromFile(toInt(json_offset), toInt(json_size));
    decrypt_1(bs, bs.length);
    System.out.println(String.format("json_offset:0x%s
json_size:0x%s", Integer.toHexString(toInt(json_offset)), Integer.toHexString(toInt(json_size))));
    System.out.println(new String(bs, StandardCharsets.UTF_8));
}

运行结果:

json_offset:0x167
json_size:0x1d81
QYVI {"qsv_info":{"ad":{},"aid":"3644740799867701","bid":600,"dr":-1,"independentaudio":true,"m3u8":"","pano":{"type":1},"qsvinfo_version":2,"sdv":"","st":"","thdt":1,"tht":0,"title":"","title_tail_info":{"bt":99,"bts":-1,"et":2621,"ete":-1,"fe":1,"le":0},"tvid":"3185901936393500","vd":{"seg":{"duration":["376140","365508","370300","371477","368311","368308","554319"],"rid":["a8c47c2906b88ae47e8503c96dae0494","4c8578cecbdfa3137a61b7e2bd1c68dc","4bc76f9b4d4ce995f40b42c467d206f8","8ed13ccd56aa2367f5eaabee12043d89","247b4612415c5b452f5965894a967e19","309400bef063cfd6baf19feb0d4a73a9","9771b1042cabfc9828767a3d0350f655"],"size":["75624801","84329329","90135782","82093900","91974933","75298884","111399115"]},"time":{"et":"2621000","st":"99000"}},"vi":"{"writer":"","authors":"","upOrder":36,"rewardAllowed":0,"fl":[],"allowEditVVIqiyi":0,"isPopup":1,"payMark":0,"an":"风吹半夏","pvu":"","subType":1,"rewardMessage":"","ipLimit":1,"pubTime":"1673306651000","mainActorRoles":[],"up":"2023-01-17 20:20:02","un":"","qiyiProduced":1,"platforms":[],"vn":"风吹半夏第1集","plc":{"4":{"downAld":1,"coa":1},"40":{"downAld":1,"coa":1},"10":{"downAld":1,"coa":1},"5":{"downAld":1,"coa":1},"41":{"downAld":1,"coa":1},"22":{"downAld":1,"coa":1},"32":{"downAld":1,"coa":1},"11":{"downAld":1,"coa":1},"12":{"downAld":1,"coa":1},"7":{"downAld":1,"coa":1},"13":{"downAld":1,"coa":1},"14":{"downAld":1,"coa":1},"91":{"downAld":1,"coa":1},"34":{"downAld":1,"coa":1},"9":{"downAld":1,"coa":1},"6":{"downAld":1,"coa":1},"16":{"downAld":1,"coa":1},"17":{"downAld":1,"coa":1},"27":{"downAld":1,"coa":1},"28":{"downAld":1,"coa":1},"18":{"downAld":1,"coa":1},"29":{"downAld":1,"coa":1},"1":{"downAld":1,"coa":1},"50":{"downAld":1,"coa":1},"31":{"downAld":1,"coa":1},"15":{"downAld":1,"coa":1},"2":{"downAld":1,"coa":1},"3":{"downAld":1,"coa":1},"21":{"downAld":1,"coa":1},"30":{"downAld":1,"coa":1},"19":{"downAld":1,"coa":1},"8":{"downAld":1,"coa":1},"20":{"downAld":1,"coa":1}},"sm":0,"st":200,"startTime":99,"showChannelId":2,"vu":"http://www.iqiyi.com/v_uvxuws1kkw.html","povu":"","ppsuploadid":0,"qiyiPlayStrategy":"非会员每日20:00转免1集","ar":"内地","cc":0,"videoQipuId":3185901936393500,"albumQipuId":3644740799867701,"pano":{"type":1},"subt":"许半夏帮童骁骑驱霉运","endTime":2621,"cpa":1,"userVideocount":0,"es":36,"plg":2774,"stl":{"d":"http://meta.video.iqiyi.com","stl":[]},"subKey":"3644740799867701","tvFocuse":"赵丽颖欧豪携手创业","qtId":0,"ty":0,"ppsInfo":{"shortTitle":"风吹半夏第1集","name":"风吹半夏第1集"},"sid":0,"etm":"","keyword":"风吹半夏","pturl":"","a":"","isTopChart":0,"idl":0,"albumAlias":"许半夏帮童骁骑驱霉运","stm":"","pd":1,"tags":[],"coa":0,"c":2,"producers":"","nurl":"","tvEname":"wild bloom","uid":"0","asubt":"","d":"傅东育|毛

关闭

用微信“扫一扫”