如何正确加密视频I帧?为何加密后P帧仍可正常播放?
问题描述
我尝试仅加密视频文件中的I帧以降低加密整体能耗,但加密后仅被加密的I帧显示为噪声,其余P帧仍可正常播放,这与预期的P帧依赖I帧应被扭曲不符。视频格式为AVI,编码为MPEG-4视频(FMP4)。
我使用FFprobe命令识别名为video1的视频的I帧:
ffprobe -select_streams v -show_frames -of csv video1.avi > video_frame_info.csv
得到的结果显示每12帧为一个I帧。随后我使用如下Python程序,通过PyCryptodome库的ChaCha20加密每12帧:
from Crypto.Cipher import ChaCha20 from Crypto.Random import get_random_bytes import cv2 import numpy as np def read_frames(video_path): # Load the video file video = cv2.VideoCapture(video_path) # Initialize a list to hold frames frames = [] # Loop until there are frames left in the video file while video.isOpened(): ret, frame = video.read() if not ret: break frames.append(frame) video.release() return frames def encrypt_iframes(frames, key): # Initialize the cipher cipher = ChaCha20.new(key=key) # Encrypt every 12th frame (considering the first frame as an I-frame) for i in range(0, len(frames), 12): frame = frames[i] # Flatten and bytes-encode the frame flat_frame = frame.flatten() bytes_frame = flat_frame.tobytes() # Encrypt the frame encrypted_frame = cipher.encrypt(bytes_frame) # Replace the original frame with the encrypted one frames[i] = np.frombuffer(encrypted_frame, dtype=flat_frame.dtype).reshape(frame.shape) return frames def write_frames(frames, output_path): # Assume all frames have the same shape height, width, _ = frames[0].shape # Create a VideoWriter object out = cv2.VideoWriter(output_path, cv2.VideoWriter_fourcc(*'mpv4'), 30, (width, height)) for frame in frames: out.write(frame) out.release() def main(): # Generate a random key key = get_random_bytes(32) # Read frames from video frames = read_frames('video1.avi') # Encrypt I-frames encrypted_frames = encrypt_iframes(frames, key) # Write encrypted frames to a new video file write_frames(encrypted_frames, 'reprise.avi') if __name__ == "__main__": main()
播放加密后的视频时,仅每12帧显示噪声,其余P帧完全正常。请问操作中的问题是什么?如何实现加密I帧后使依赖它的P帧也无法正常播放?
问题根源
你的操作核心问题在于处理的是解码后的原始帧,而非编码层面的压缩帧数据:
- 用OpenCV读取视频时,已经完成了解码过程——P帧会被自动解码成完整的原始帧,不再依赖原始I帧的压缩数据。
- 你加密的是解码后的I帧原始像素数据,写入新视频时,OpenCV会重新对所有帧进行编码,此时每个P帧都是基于前一个已解码的原始帧生成的,和你加密的原I帧压缩数据没有关联。
简单说:你相当于把视频拆成了一堆独立的原始图片,加密其中几张后,又重新编码成了一个新的、帧间依赖关系完全重置的视频,自然P帧不受影响。
解决方案
要实现加密I帧后让依赖它的P帧失效,必须在编码后的压缩数据流层面操作,而非解码后的原始帧。具体步骤如下:
1. 提取视频的原始压缩码流
用FFmpeg提取视频的H.264/MPEG-4压缩码流:
ffmpeg -i video1.avi -c:v copy -bsf:v h264_mp4toannexb -f rawvideo video1.h264
2. 定位码流中的I帧位置
通过FFprobe获取I帧的起始字节偏移和长度(而非帧序号):
ffprobe -select_streams v -show_frames -show_entries frame=pkt_pos,pkt_size,key_frame -of csv video1.avi > frame_metadata.csv
在输出的CSV中,key_frame=1的行就是I帧,记录对应的pkt_pos(起始字节)和pkt_size(字节长度)。
3. 加密码流中的I帧数据
编写Python代码直接操作H.264码流文件,找到I帧的字节区间并加密:
from Crypto.Cipher import ChaCha20 from Crypto.Random import get_random_bytes def encrypt_h264_iframes(input_path, output_path, key): # 读取帧元数据 iframe_positions = [] with open('frame_metadata.csv', 'r') as f: next(f) # 跳过表头 for line in f: parts = line.strip().split(',') if len(parts) >= 4 and parts[3] == '1': # 筛选I帧 pkt_pos = int(parts[1]) pkt_size = int(parts[2]) iframe_positions.append((pkt_pos, pkt_size)) # 读取整个码流文件 with open(input_path, 'rb') as f: stream_data = bytearray(f.read()) # 加密每个I帧的字节区间 cipher = ChaCha20.new(key=key) for pos, size in iframe_positions: iframe_data = stream_data[pos:pos+size] encrypted_data = cipher.encrypt(iframe_data) stream_data[pos:pos+size] = encrypted_data # 写入加密后的码流 with open(output_path, 'wb') as f: f.write(stream_data) def main(): key = get_random_bytes(32) encrypt_h264_iframes('video1.h264', 'encrypted_video.h264', key) if __name__ == "__main__": main()
4. 重新封装成视频文件
用FFmpeg把加密后的H.264码流封装回AVI格式:
ffmpeg -i encrypted_video.h264 -c:v copy encrypted_output.avi
这样处理后,播放加密视频时,I帧的压缩数据被破坏,解码器无法正确解码I帧,后续依赖该I帧的P帧也会因为参考数据错误而无法正常显示,全部呈现噪声或扭曲。
内容的提问来源于stack exchange,提问作者user21612887

