Принцип реализации воспроизведения APNG в сети

анимация Canvas
Принцип реализации воспроизведения APNG в сети

Источник титульного изображения:commons.wikimedia.org

Автор этой статьи: Ян Цайфан

написать впереди

При живой разработке облачной музыки часто возникает потребность в воспроизведении анимации, сценарии применения каждого требования разные, и большинство анимаций с меньшими объемами в формате APNG.

Может использоваться, если анимация отображается только одна<img>Анимация APNG отображается напрямую, но будут ошибки совместимости, например, некоторые браузеры не поддерживают воспроизведение APNG, и повторное воспроизведение некоторых моделей Android не удастся.

Если для отображения анимации необходимо объединить анимацию APNG и другие элементы DOM с анимацией CSS3, APNG необходимо предварительно загрузить и контролировать. Предварительная загрузка может сократить время анализа APNG, что приведет к проблеме асинхронности между ними. Управление может помочь пользователям APNG Выполнение некоторых операций в узлах времени, таких как успешный синтаксический анализ или окончание воспроизведения.

эти вопросыapng-canvasможет помочь нам решить ее. apng-canvas использует холст для рисования анимации APNG, которая совместима с большим количеством браузеров, сглаживает различия между разными браузерами и позволяет легко управлять воспроизведением APNG. Далее будут подробно представлены принципы реализации библиотек APNG и apng-canvas, а также реализация рендеринга WebGL на основе apng-canvas.

Введение в APNG

APNG (Animated Portable Network Graphics, Animated PNG) — это формат растровой анимации, основанный на расширении формата PNG, в котором добавлена ​​поддержка анимированных изображений, а также добавлена ​​поддержка 24-битных изображений в истинном цвете и 8-битной альфа-прозрачности хорошего качества. APNG сохраняет обратную совместимость с традиционным PNG и отображает изображение по умолчанию, когда декодер не поддерживает воспроизведение APNG.

Помимо APNG, распространенными форматами анимации являются GIF и WebP. С точки зрения совместимости с браузером, размера и качества изображения результаты следующие (размер изображения взят в качестве примера, а сравнение размеров других однотонных или цветных изображений можно просмотретьGIF vs APNG vs WebP, APNG в большинстве случаев меньше). Всестороннее сравнение APNG лучше, поэтому мы выбираем APNG.

Структура APNG

APNG — это расширение, основанное на формате PNG.Давайте сначала разберемся со структурой PNG.

Состав структуры PNG

PNG в основном включаетPNG Signature,IHDR,IDAT,IENDи некоторые вспомогательные блоки. в,PNG Signature— идентификатор файла, который используется для проверки того, является ли файл форматом PNG;IHDRЭто блок данных заголовка файла, который содержит основную информацию об изображении, такую ​​как ширина и высота изображения;IDATЭто блок данных изображения, в котором хранятся определенные данные изображения. PNG-файл может иметь один или несколькоIDATКусок;IEND— конечный блок данных, обозначающий конец изображения, вспомогательный блок находится вIHDRПозжеIENDРанее спецификация PNG не накладывала на него ограничений по сортировке.

PNG SignatureРазмер блока 8 байт, а содержимое следующее:

0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a

Структура каждого другого блока в основном следующая:

4 байта определяют длину данных, 4 байта определяют тип блока, байты длины — это данные (если длина данных равна 0, такой части нет), а последние 4 байта — это проверка CRC.

Состав структуры APNG

APNG добавляет к PNGacTL,fcTLа такжеfdATСуществует 3 вида блоков, и их структура показана на следующем рисунке:

  • acTL: Блок управления анимацией, который содержит количество кадров и циклов изображения (0 означает бесконечный цикл)
  • fcTL: Блок управления кадром, относящийся к вспомогательному блоку в спецификации PNG, содержит порядковый номер текущего кадра, ширину и высоту изображения, смещения по горизонтали и вертикали, время воспроизведения кадра и метод рисования (dispose_op и blend_op) и т. д. Каждый кадр имеет только одинfcTLКусок
  • fdAT: Блок данных кадра, который содержит серийный номер и данные изображения кадра, только по сравнению сIDATСуществует больше порядковых номеров кадров, каждый кадр может иметь один или несколькоfcTLКусок.fdATсерийный номер сfcTLОбщий, используется для обнаружения ошибок последовательности в APNG, опционально исправляется

IDATБлоки — это изображения по умолчанию, когда APNG отображается для обратной совместимости. еслиIDATраньшеfcTL, ТакIDATДанные рассматриваются как изображение первого кадра (структура, как указано выше), еслиIDATНет раньшеfcTL, то первая картинка кадра будет первойfdAT,Как показано ниже:

Воспроизведение анимации APNG в основном происходит черезfcTLЧтобы управлять изображением, отображаемым для каждого кадра, то есть управлять методом рисования через dispose_op и blend_op.

  • dispose_op указывает операцию над буфером перед отрисовкой следующего кадра

    • 0: не очищать холст и отображать новые данные изображения напрямую в области, указанной холстом.

    • 1: очистить холст в области текущего кадра до цвета фона по умолчанию перед рендерингом следующего кадра

    • 2: В следующем кадре перед рендерингом восстановленный результат отображается в области холста в текущем кадре.

  • blend_op указывает операцию над буфером перед отрисовкой текущего кадра

    • 0: Указывает, что текущая область очищается, а затем рисуется

    • 1: Указывает, что текущая область рисуется напрямую без очистки, а изображение накладывается

принцип реализации apng-canvas

Разобравшись со структурой APNG, мы можем проанализировать принцип реализации apng-canvas, который в основном делится на две части: декодирование и рендеринг.

декодирование APNG

Процесс декодирования APNG показан на следующем рисунке:

Сначала преобразуйте APNG вarraybufferСкачать ресурсы в формате视图Работа с двоичными данными; затем проверьте, является ли формат файла PNG и APNG по очереди; затем разделите каждую часть APNG по очереди для обработки и сохранения; наконец, разделите полученный блок метки PNG, блок заголовка, другие вспомогательные блоки и данные изображения кадра одного кадра Блок и конечный блок перекомпоновывают изображение PNG и загружают ресурс изображения. В этом процессе требуется поддержка браузераTyped Arraysа такжеBlob URLs.

Ресурс файла APNG получается черезXMLHttpRequestСкачайте, реализация проста, и я не буду здесь вдаваться в подробности.

Проверьте формат PNG

Проверьте, что формат PNG проверенPNG Signatureблока сравниваем содержимое первых 8 байт файлового ресурса с первого байта по очереди.Реализация ключа следующая:

const bufferBytes = new Uint8Array(buffer); // buffer为下载的arraybuffer资源
const PNG_SIGNATURE_BYTES = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
for (let i = 0; i < PNG_SIGNATURE_BYTES.length; i++) {
    if (PNG_SIGNATURE_BYTES[i] !== bufferBytes[i]) {
        reject('Not a PNG file (invalid file signature)');
        return;
    }
}

Проверьте формат APNG

Проверка формата APNG заключается в том, чтобы определить, существует ли файл с типомacTLблокировать. Следовательно, необходимо прочитать каждый блок в файле, чтобы получить такие данные, как тип блока. Чтение блока обрабатывается в соответствии с базовой структурой блока PNG, описанной выше, и этот процесс реализован, как показано на следующем рисунке:

Начальное значение off равно 8, т.е.PNG Signatureразмер в байтах, а затем читать каждый блок последовательно. Сначала прочитайте 4 байта, чтобы получить длину блока данных, продолжайте читать 4 байта, чтобы получить тип блока данных, затем выполните функцию обратного вызова для обработки данных этого блока и оцените, необходимо ли это в соответствии с возвращаемым значением res. , тип блока и значение отключения функции обратного вызова Продолжить чтение следующего блока (значение res указывает, следует ли продолжать чтение следующего блока данных, значение по умолчанию равноundefinedпродолжить чтение). Если вы продолжите, значение off будет накапливаться.4 + 4 + length + 4, Смещение к началу следующего блока для выполнения цикла, в противном случае он заканчивается напрямую. Код ключа следующий:

const parseChunks = (bytes, callback) => {
    let off = 8;
    let res, length, type;
    do {
        length = readDWord(bytes, off);
        type = readString(bytes, off + 4, 4);
        res = callback(type, bytes, off, length);
        off += 12 + length;
    } while (res !== false && type !== 'IEND' && off < bytes.length);
};

передачаparseChunksНайти с нуля, как только он существуетtype === 'acTL'блок вернетсяfalseХватит читать, ключевая реализация выглядит следующим образом:

let isAnimated = false;
parseChunks(bufferBytes, (type) => {
    if (type === 'acTL') {
        isAnimated = true;
        return false;
    }
    return true;
});
if (!isAnimated) {
    reject('Not an animated PNG');
    return;
}

Обработайте каждый блок по типу

Подробная структура блока основного типа в структуре APNG показана на следующем рисунке:

передачаparseChunksПрочитайте каждый блок по очереди, обработайте и сохраните данные в соответствии с данными, содержащимися в каждом типе блока, и его соответствующему смещению и размеру байта. который обрабатываетfcTLа такжеfdATЧтение порядкового номера кадра (sequence_number) при блокировке пропускается, и похоже проблема ошибки порядкового номера не рассматривается. Ключевая реализация выглядит следующим образом:

let preDataParts = [], // 存储 其他辅助块
    postDataParts = [], // 存储 IEND块
    headerDataBytes = null; // 存储 IHDR块

const anim = anim = new Animation();
let frame = null; // 存储 每一帧

parseChunks(bufferBytes, (type, bytes, off, length) => {
    let delayN,
        delayD;
    switch (type) {
        case 'IHDR':
            headerDataBytes = bytes.subarray(off + 8, off + 8 + length);
            anim.width = readDWord(bytes, off + 8);
            anim.height = readDWord(bytes, off + 12);
            break;
        case 'acTL':
            anim.numPlays = readDWord(bytes, off + 8 + 4); // 循环次数
            break;
        case 'fcTL':
            if (frame) anim.frames.push(frame); // 上一帧数据
            frame = {}; // 新的一帧
            frame.width = readDWord(bytes, off + 8 + 4);
            frame.height = readDWord(bytes, off + 8 + 8);
            frame.left = readDWord(bytes, off + 8 + 12);
            frame.top = readDWord(bytes, off + 8 + 16);
            delayN = readWord(bytes, off + 8 + 20);
            delayD = readWord(bytes, off + 8 + 22);
            if (delayD === 0) delayD = 100;
            frame.delay = 1000 * delayN / delayD;
            anim.playTime += frame.delay; // 累加播放总时长
            frame.disposeOp = readByte(bytes, off + 8 + 24);
            frame.blendOp = readByte(bytes, off + 8 + 25);
            frame.dataParts = [];
            break;
        case 'fdAT':
            if (frame) frame.dataParts.push(bytes.subarray(off + 8 + 4, off + 8 + length));
            break;
        case 'IDAT':
            if (frame) frame.dataParts.push(bytes.subarray(off + 8, off + 8 + length));
            break;
        case 'IEND':
            postDataParts.push(subBuffer(bytes, off, 12 + length));
            break;
        default:
            preDataParts.push(subBuffer(bytes, off, 12 + length));
    }
});
if (frame) anim.frames.push(frame); // 依次存储每一帧帧数据

сборка png

После разделения блоков данных вы можете собрать PNG, пройдяanim.framesОбъедините блоки общих данных PNG PNG_SIGNATURE_BYTES, headerDataBytes, preDataParts, dataParts данных кадра одного кадра и postDataParts в ресурс изображения PNG (bb) последовательно, черезcreateObjectURLURL созданного изображения сохраняется в кадре для последующего рисования.

const url = URL.createObjectURL(new Blob(bb, { type: 'image/png' }));
frame.img = document.createElement('img');
frame.img.src = url;
frame.img.onload = function () {
    URL.revokeObjectURL(this.src);
    createdImages++;
    if (createdImages === anim.frames.length) { //全部解码完成
        resolve(anim);
    }
};

На этом мы закончили работу по расшифровке, вызываемAPNG.parseUrlМожет быть реализована функция предварительной загрузки ресурса анимации: после инициализации страницы ресурс загрузки вызывается в первый раз, и он вызывается снова при рендеринге, а результат синтаксического анализа возвращается непосредственно для выполнения операции рисования.

const url2promise = {};
APNG.parseURL = function (url) {
    if (!(url in url2promise)) {
        url2promise[url] = loadUrl(url).then(parseBuffer);
    }
    return url2promise[url];
};

Розыгрыш APNG

После декодирования APNG его можно рисовать и воспроизводить в соответствии с блоком управления анимацией и блоком управления кадром. В частности, используйте requestAnimationFrame для отрисовки каждого кадра изображения по очереди на холсте для воспроизведения. apng-canvas использует 2D-рендеринг Canvas.

const tick = function (now) {
    while (played && nextRenderTime <= now) renderFrame(now);
    if (played) requestAnimationFrame(tick);
};

2D-рисование на холсте в основном использует Canvas 2D API.drawImage,clearRect,getImageData,putImageDataвыполнить.

const renderFrame = function (now) {
    // fNum 记录循环播放时的总帧数
    const f = fNum++ % ani.frames.length;
    const frame = ani.frames[f];
    // 动画播放结束
    if (!(ani.numPlays === 0 || fNum / ani.frames.length <= ani.numPlays)) {
        played = false;
        finished = true;
        if (ani.onFinish) ani.onFinish(); // 这行是作者加的便于在动画播放结束后执行一些操作
        return;
    }

    if (f === 0) {
        // 绘制第一帧前将动画整体区域画布清空
        ctx.clearRect(0, 0, ani.width, ani.height);  
        prevF = null; // 上一帧
        if (frame.disposeOp === 2) frame.disposeOp = 1;
    }

    if (prevF && prevF.disposeOp === 1) { // 清空上一帧区域的底图
        ctx.clearRect(prevF.left, prevF.top, prevF.width, prevF.height);
    } else if (prevF && prevF.disposeOp === 2) { // 恢复为上一帧绘制之前的底图
        ctx.putImageData(prevF.iData, prevF.left, prevF.top);
    } // 0 则直接绘制

    const {
        left, top, width, height,
        img, disposeOp, blendOp
    } = frame;
    prevF = frame;
    prevF.iData = null;
    if (disposeOp === 2) { // 存储当前的绘制底图,用于下一帧绘制前恢复该数据
        prevF.iData = ctx.getImageData(left, top, width, height);
    }
    if (blendOp === 0) { // 清空当前帧区域的底图
        ctx.clearRect(left, top, width, height);
    }

    ctx.drawImage(img, left, top); // 绘制当前帧图片

    // 下一帧的绘制时间
    if (nextRenderTime === 0) nextRenderTime = now;
    nextRenderTime += frame.delay; // delay为帧间隔时间
};

WebGL-рендеринг

Метод рендеринга может использовать WebGL в дополнение к Canvas 2D. Производительность рендеринга WebGL лучше, чем у Canvas 2D, но WebGL не имеет API, который может напрямую рисовать изображения, а код реализации рисования более сложен. В этой статье не будет показан конкретный код для рисования изображений.drawImageРеализация WebGL API может относиться кWebGL-drawimage,двумерная матрицаЖдать. Далее будут представлены ключевые моменты выбранной автором схемы реализации рендеринга.

Поскольку WebGL неgetImageData,putImageDataТакие API могут получать или копировать данные изображения текущего холста, поэтому несколько текстур инициализируются при инициализации WebGL, а переменная glRenderInfo используется для записи данных текстуры исторического рендеринга.

// 纹理数量
const textureLens = ani.frames.filter(item => item.disposeOp === 0).length;

// 历史渲染的纹理数据
const glRenderInfo = {
    index: 0,
    frames: {},
};

При рендеринге каждого кадра в соответствии сglRenderInfo.framesРендеринг с несколькими текстурами последовательно с одновременным обновлениемglRenderInfoданные.

const renderFrame = function (now) {
    ...
    let prevClearInfo;
    if (f === 0) {
        glRenderInfo.index = 0;
        glRenderInfo.frames = {};
        prevF = null;
        prevClearInfo = null;
        if (frame.disposeOp === 2) frame.disposeOp = 1;
    }
    if (prevF && prevF.disposeOp === 1) { //  需要清空上一帧区域底图
        const prevPrevClear = glRenderInfo.infos[glRenderInfo.index].prevF;
        prevClearInfo = [
            ...(prevPrevClear || []),
            prevF,
        ];
    }
    if (prevF && prevF.disposeOp === 0) { // 递增纹理下标序号,否则直接替换上一帧图片
        glRenderInfo.index += 1;
    }
    // disposeOp === 2 直接替换上一帧图片
    glRenderInfo.frames[glRenderInfo.index] = { // 更新glRenderInfo
        frame,
        prevF: prevClearInfo, // 用于清除上一帧区域底图
    };
    prevF = frame;
    prevClearInfo = null;
    // 绘制图片,底图清除在 glDrawImage 接口内部实现
    Object.entries(glRenderInfo.frames).forEach(([key, val]) => {
        glDrawImage(gl, val.frame, key, val.prevF);
    });
    ...
}

резюме

В этой статье представлены структура и состав APNG, декодирование изображений и реализация рендеринга с использованием Canvas 2D/WebGL. Я надеюсь, что после прочтения этой статьи она может быть вам полезна, добро пожаловать на обсуждение.

Ссылаться на

Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы набираем front-end, iOS и Android круглый год.Если вы готовы сменить работу и любите облачную музыку, присоединяйтесь к нам на grp.music-fe(at)corp.netease.com!