VideoHelp Forum




+ Reply to Thread
Results 1 to 9 of 9
  1. Hello peeps. Sorry If I can't explain this correctly.

    My ISP Telekom has a streaming website (Magenta TV Go or something) where you can watch the local and not so local TV channels live 24/7. Even after the live ends, you can go back and watch previous hours of almost any of the TV channels, up to 7 days or so.

    So let's say I want something that aired 10 hours ago, I just simply click the show I want and I can easily download it and decrypt it by getting the keys/commands with WidevineProxy extension.

    Unfortunately, during the livestream (and also obviously afterwards when you can go back) the key changes every 12 hours or so.

    So let's say the key changes 6pm/am. If the particular show/part I want to download aired between 06:01 am and 17:59 pm there's no trouble getting it, only 1 key, downloads and decrypts fine.

    However, this particular show I want to download airs from 5pm to 8pm. In 6pm the key changes.
    When I try to download with the first set of keys that are before the change, N_m3u8-RE downloads the 3 hours, decrypts it but when I play it, I can only see the decrypted content for the first hour (up until the key change in 6pm) and then the rest 2 hours are encrypted.
    When I try to download with the 2nd set of keys that are after the change, N_m3u8-RE downloads, decrypts it but when I play it, everything is encrypted.

    I tried to decrypt the stream with all of the keys, it doesn't work. Tried decrypting with both shaka and mp4decrypt, doesn't work.

    What I find weird is that when the key changes, there doesn't seem to be an init file change or a manifest/mpd change.
    When I download the init file from the beginning of the stream, concatenate it with a video segment from the first hour, then decrypt that small file with the first set of keys, it decrypts and plays fine.
    When I try to concatenate a video segment that shows AFTER the key change to this init file, and try to decrypt it with any of the keys, it doesn't work.

    I'm at a loss. Any help would be truly appreciated.
    Have a great day.
    click click2
    If I/my posts ever helped you, and you want to give back, send me a private message!
    Quote Quote  
  2. This sounds fun!
    To debug this further, could you share both a segment before and after the key change and the init (and the keys)? Just changing decryption keys should be possible without a new init file.
    Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2
    Quote Quote  
  3. Originally Posted by larley View Post
    This sounds fun!
    To debug this further, could you share both a segment before and after the key change and the init (and the keys)? Just changing decryption keys should be possible without a new init file.
    Hey larley, I like how this sounds fun to you yet brings a lot of frustration in me, haha.
    You can get the files from here
    https://www.swisstransfer.com/d/8b3a7f32-8e57-4761-adce-c63874508889
    click click2
    If I/my posts ever helped you, and you want to give back, send me a private message!
    Quote Quote  
  4. Ok, the solution is somewhat simple. All we need to do is switch out the key ID in the tenc box in the init:
    https://www.swisstransfer.com/d/88b09935-0a33-48f9-934e-96634f0d7103

    I'll get to writing a little python script for you.
    Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2
    Quote Quote  
  5. Originally Posted by larley View Post
    I'll get to writing a little python script for you.
    That'd be excellent larley. Switch out the key ID to what?
    Ah, right, found the key ID in the init with HxD and modified it with the other key id from the 2nd key and it decrypted fine.
    But a script would be nice!
    Last edited by [ss]vegeta; 16th Mar 2026 at 10:12.
    click click2
    If I/my posts ever helped you, and you want to give back, send me a private message!
    Quote Quote  
  6. idk how I know this.
    init always have the same discarded kid.
    new segments have the newer kid when appended to init segment needs new keys.

    I think N_m3u8DL-Re fails because it is appending each segment into one first segment.
    when the keys change it needs to be appended into the init again.
    discord=notaghost9997
    Quote Quote  
  7. 9daa62884ef28d78b7e4aafee46c30d9
    init.mp4
    Quote Quote  
  8. Originally Posted by [ss]vegeta View Post
    That'd be excellent larley. Switch out the key ID to what?
    Ah, right, found the key ID in the init with HxD and modified it with the other key id from the 2nd key and it decrypted fine.
    But a script would be nice!

    We take the the key ID from the moof/traf/sgpd SampleGroupDescriptionEntry of grouping_type "seig" and copy it to moov/trak/mdia/minf/stbl/stsd/{enc_sample_entry}/sinf/schi/tenc. That's all we need to change to re-use the init from the beginning of the stream.

    Videos like these can be fixed with the script:
    - init before the key change
    - fragments after the key change
    and they decrypt correctly.

    But if we have a file like:
    - init before key change
    - fragments before key change
    - init before key change
    - fragments after key change
    the script will still apply fixes like they should work but the resulting file is not decryptable. This may be caused by the fragments you provided not really coming after one another in the stream or just mp4decrypt/shaka-packager not supporting multiple inits per media file. So you'll need to still split the recorded video at the "key change boundary".

    Code:
    import argparse
    from os import SEEK_CUR
    
    
    def peek_box(f) -> tuple[str, int]:
        box_size = int.from_bytes(f.read(4))
        box_type = f.read(4).decode("latin-1")
        f.seek(-8, SEEK_CUR)
        return box_type, box_size
    
    
    def enter_box(f) -> int:
        start_pos = f.tell()
    
        box_size = int.from_bytes(f.read(4))
        box_type = f.read(4).decode("latin-1")
    
        if box_size == 1:
            box_size = int.from_bytes(f.read(8))
    
        if box_type == "uuid":
            f.seek(16, SEEK_CUR)
    
        special_containers = {
            "stsd": 4 + 4,
            "encv": 8 + 70,
            "enca": 8 + 20,
            "encm": 8,
            # enct, encu, encs
            "encf": 8,
            "encp": 8,
            "enc3": 8 + 32
        }
    
        if box_type in special_containers:
            f.seek(special_containers[box_type], SEEK_CUR)
    
        return box_size - (f.tell() - start_pos)
    
    
    def edit_tenc(f, key_id: bytes) -> None:
        start_pos = f.tell()
    
        enter_box(f)
        f.seek(4, SEEK_CUR)
        f.seek(4, SEEK_CUR)
        f.write(key_id)
    
        f.seek(start_pos)
    
    
    def read_kid_sgpd_seig(f) -> bytes | None:
        start_pos = f.tell()
    
        enter_box(f)
        version = f.read(1)[0]
        f.seek(3, SEEK_CUR)
    
        grouping_type = f.read(4).decode("latin-1")
    
        if grouping_type != "seig":
            return None
    
        default_length = None
        if version >= 1:
            default_length = int.from_bytes(f.read(4))
        if version >= 2:
            f.seek(4, SEEK_CUR)
    
        f.seek(4, SEEK_CUR)
        if version >= 1 and default_length == 0:
            f.seek(4, SEEK_CUR)
    
        # we only support a single SampleGroupDescriptionEntry
    
        byte1 = f.read(1)[0]
        multi_key_flag = (byte1 >> 7) & 0x01
    
        f.seek(2, SEEK_CUR)
    
        if multi_key_flag > 0:
            f.seek(2, SEEK_CUR)
    
        # we only support a single key
    
        f.seek(1, SEEK_CUR)
        kid = f.read(16)
    
        f.seek(start_pos)
    
        return kid
    
    
    def find_box_iterative(f, target_box: str, max_depth: int = 128) -> int | None:
        iters = 0
        while True:
            if iters >= max_depth:
                return None
    
            box_type, box_size = peek_box(f)
            if box_type == target_box:
                break
    
            f.seek(box_size, SEEK_CUR)
    
            iters += 1
    
        return box_size
    
    
    def seek_to_box(f, path: str, max_depth: int = 128) -> dict[str, int] | None:
        containers = path.split("/")
        target_box = containers.pop(-1)
    
        # print("start:", containers, target_box)
        container_sizes = {}
    
        iters = 0
        while len(containers) > 0:
            # print("containers", containers)
    
            if iters >= max_depth:
                return None
    
            box_type, box_size = peek_box(f)
    
            if box_type != containers[0]:
                f.seek(box_size, SEEK_CUR)
            else:
                container_sizes[box_type] = box_size
                # print(f.tell(), box_type)
    
                enter_box(f)
                containers.pop(0)
    
            iters += 1
    
        find_box_iterative(f, target_box, max_depth)
    
        return container_sizes
    
    
    def fix_tenc(file: str, enc_sample_entry: str):
        with open(file, "r+b") as f:
            while True:
                moov_size = find_box_iterative(f, "moov", 128)
                if moov_size is None:
                    break
    
                moov_pos = f.tell()
                f.seek(moov_size, SEEK_CUR)
    
                moof_sizes = seek_to_box(f, "moof/traf/sgpd")
                if moof_sizes is None:
                    break
    
                key_id = read_kid_sgpd_seig(f)
                print("fixing key ID", key_id.hex())
    
                f.seek(moov_pos)
    
                moov_sizes = seek_to_box(f, f"moov/trak/mdia/minf/stbl/stsd/{enc_sample_entry}/sinf/schi/tenc")
                if moov_sizes is None:
                    break
    
                edit_tenc(f, key_id)
    
                f.seek(moov_pos + moov_sizes["moov"] + moof_sizes["moof"])
    
            print("done")
    
    
    if __name__ == '__main__':
        parser = argparse.ArgumentParser()
        parser.add_argument("file", type=str)
        parser.add_argument("type", choices=["video", "audio"])
        args = parser.parse_args()
    
        sample_entry = {
            "video": "encv",
            "audio": "enca"
        }[args.type.lower()]
    
        fix_tenc(args.file, sample_entry)
    Usage:
    Code:
    usage: fix_tenc_stream.py [-h] file {video,audio}
    
    positional arguments:
      file
      {video,audio}
    
    options:
      -h, --help     show this help message and exit
    Bypass HMACs, One-time-tokens and Lic.Wrapping: https://github.com/DevLARLEY/WidevineProxy2
    Quote Quote  
  9. Thanks a lot larley!
    Truly appreciate your effort and help.
    click click2
    If I/my posts ever helped you, and you want to give back, send me a private message!
    Quote Quote  



Similar Threads

Visit our sponsor! Try DVDFab and backup Blu-rays!