试着记录一下当初解析 mpls 的文件结构时经历的过程。

mpls, 即指蓝光的播放列表, 该文件以二进制的形式保存了包括播放顺序, 播放对应的视频、音频、字幕、章节信息

我最初的目标仅仅是要获取其中的章节信息, 在BD Chapters MOD这里找到了一个可用的实现, 这个程序获取章节的姿势略微奇巧, 因为mpls中的章节是位于文件的结尾处并且有一定的特点的, 以形如 0001 yyyy xxxxxxxx FFFF 000000 来表示一个时间戳, 并且同一个视频片段的时间戳是连续的, 于是, 在那个程序里就会发现这样一个函数.


CHAPTER_MARK = b'\xff\xff\x00\x00\x00\x00'
PALY_MARK_TYPE = b'\x01'

...

def get_playlist(mpls):
    input = open(mpls, 'rb')
    content = input.read()
    bytelist = []
    ptsinfo = []
    multi_ptsinfo = []
    #read the 14 bytes from buttom eachtime
    #eg. 0001 yyyy xxxxxxxx FFFF 000000
    offset = content.rfind(CHAPTER_MARK)
    offset = len(content) - offset - 6

    pl_index=0
    pre_m2ts_index = -1

    while True:
        pl_index += 1
        input.seek(-14*pl_index-offset, 2)

        bytelist = []
        for x in range(14):
            bytelist.append(input.read(1))

        m2ts_index = str2int(bytelist[2:4])
        if m2ts_index != pre_m2ts_index:
            ptsinfo = []
            cinfo = ChapInfo(ptsinfo, m2ts_index)
            multi_ptsinfo.append(cinfo)
            pre_m2ts_index = m2ts_index

        #last 6 bits: ff ff 00 00 00 00 per section
        if ord(bytelist[13]) != 0:
            break
        if bytelist[1] == PALY_MARK_TYPE:
            ptsinfo.insert(0, str2int(bytelist[4:8]))

        if len(ptsinfo)>1 and ptsinfo[-1] == ptsinfo[-2]:
            ptsinfo.pop(-1)
            break

...

    input.close()
    return multi_ptsinfo

于是我将这个程序转写成C#并将之实作于ChapterTool中, 如果这个能够一直正确地使用下去的话, 那也不会有我重写的事情了.

有一天, 探姬同我讲灰色的果实原盘里的mpls所提取的章节与视频文件的文件名不匹配, 这个问题, 经过我一番探查, 涉及到另一个函数.

SEARCH_M2TS = b'M2TS'

...

def get_m2ts_list(mpls):
    m2ts_list = []
    input = open(mpls, 'rb')
    content = input.read()
    input.close()

    pos_from = 0

    while True:
        pos = content.find(SEARCH_M2TS, pos_from)
        if pos == -1:
            break
        m2ts_name = content[pos-5:pos+4]

...

        m2ts_name = str(m2ts_name, encoding='ascii')
        m2ts_list.append(m2ts_name)
        pos_from = pos + 4
    return m2ts_list

这里也是是使用文件名后面必定存在的m2ts来做文章, 找出所有的标记从而获得mpls中的所有的视频文件的文件名. 看起来十分地合理, 可是这个函数遗漏了一种略为少见的情况, 某个playlist存在多个angle.

这里的这个angle类似于分支, 通过选项可以使得视频的播放中的某一段有多个不同的片段供播放, 从而实现NCOP/多结局等效果而不必大费周章地再生成一个新的playlist.

那么, 多angle必然导致mpls中在对应的片段中相比普通的一个视频文件变成 angle个视频文件, 于是章节名列表中就会多出几个, 从而使得通过PlaylistMark中给定的index获得文件名会出现不对应的情况. 可是, 这已经可以归属于方法上的硬伤了, 通过这种搜索标记的方法必然无法获得足够的信息以判定这个文件是否为多angle中的一个.

这样分析之后, 就足以判明通过打补丁的方法是不足以更正这个问题的, 需要用另外的方法来获取更多的信息从而判断各个章节的归属.

起初我搜索的姿势还不够纯熟, 找到了这个页面wikibooks/mpls, 这个页面上有一部分关于mpls文件结构的描述.

有总比没有强, 于是我就开始了解析工作. 说起来其实并不是很困难, 照着页面中说的读取就行了, 但是其中缺了很多东西, 甚至连章节名之类的在哪里都没有说. 我当时采取的方法是: 使用BDEdit来更改mpls文件, 观察更改前后的变化从而得知各个地方的数据的作用是什么, 这样一个字节一个字节地抠, 从而补上了Play Item section,stream entry,stream attributes这些信息, 并尽可能的把每一个字节的功用记录下来.

这样, 差不多章节读取功能就算是实现了, 然而多angle相关的信息我还是不甚了解, 于是我就先退而求其次, 用硬编码, 如果发现是多angle的将Play Item Entry向后移0x3e即默认所有多angle我只当是2(好懒), The Grand Budapest Hotel的原盘一些片段有4个angle, 据说还有7个angel的, 分别显示不同的语言的标识, 手动害怕.

后来, 在准备往ChapterTool中加入对ifo格式章节的解析的时候, 找到了Chapter Grabber, 从中参考了很多代码, 也将一个mpls生成一个完整的章节的功能也加了上去, 虽然Chapter Grabber中有跳过多angle的操作, 但是他的代码结构与我的不尽相同, 难以直接套用.

//ignore angles
streamFileOffset += 2 +
    ((int)playlistData[streamFileOffset] << 8) +
    ((int)playlistData[streamFileOffset + 1]);

再后来, 找到了完整的mpls spec, 详尽到我驾驭不住了, 在仔细参阅后明白了这个多angle对每一个angle需要跳多长, 对不同的stream对应的language code的位置在哪里等等之前一直不明白的问题.

//_data 储存整个mpls文件的byte数据的数组
//playItemEntries playItem的起点位置

var lengthOfPlayItem = Byte2Int16(_data, playItemEntries);
var bytes = new byte[lengthOfPlayItem + 2];
Array.Copy(_data, playItemEntries, bytes, 0, lengthOfPlayItem);

...

var isMultiAngle = (bytes[0x0c] >> 4) & 0x01;
StringBuilder nameBuilder  = new StringBuilder(Encoding.ASCII.GetString(bytes, 0x02, 0x05));

if (isMultiAngle == 1) //skip multi-angle
{
    int numberOfAngles = bytes[0x22];
    for (int i = 1; i < numberOfAngles; i++)
    {
        nameBuilder.Append("&" + Encoding.ASCII.GetString(bytes, 0x24+(i-1)*0x0a, 0x05));
    }
    itemStartAdress = playItemEntries + 0x02 + (numberOfAngles - 1) * 0x0a;
}

最近又发现了一个新的怪问题, 就是在绯弹的亚里亚AA的mpls中有多个视频片段的时候, 第二个的章节的起点不是视频的00:00:00.000处, 而是更加稍后的位置, 在亚里亚AA里是偏移了24帧, 类似这样----------=|=========. 其实这个也不算是个麻烦事, 只是说之前没见过. 这里, 应该使用mpls中给出的该片段起点时间戳作为章节的起点即可, 这种处理方法目前尚未发现问题.

完整代码详见mplsData.cs


随着UHD的蓝精灵2宣告被破解,我之前的这个补丁摞补丁的代码终于撑不住了,于是就对parser按照spec展开了重写。

相较于以前两眼摸黑,这回就接近于纯体力活了,unlimited class define work(雾

除了ExtensionData以外的所有结果都恰当地读了出来。也发现了version3与version2之间的区别,STNTable中的元素尾部多了些padding,以往是没有的,于是旧代码中默认每个元素之间紧贴,读出了一堆0。

说起来在这个时间点2017-05-06还没[几个|有]工具支持UHD的的章节读取呢。

乘机把ChapterTool的一部分历史遗留代码重构了一下,稍微工整一点了。嘛,就算用wpf重写一遍,也没什么很大的好处,就继续WinForm丑下去吧(雾

著作权声明

作者 TautCony,首次发布于 TC Blog,转载请保留以上链接