Уверен, что многих возмутит уже самоназвание этой статьи. А некоторые сразу же побегут в комментарии указывать на приложение, которое «смогло». Но не стоит спешить, друзья! Сегодня вам предстоит увлекательное путешествие по стыку технологий, кода и технических решений, которые и расскажут вам то, о чем адепты съемки мобильного RAW-видео предпочитают не говорить.

Я разберу лишь основные моменты, которые и убедили меня в том, что эффективная съемка RAW‑видео на Андроид на сегодняшний день невозможна без »костылей» и ухищрений. Костылей, которые нивелируют все те преимущества RAW, которые так жаждут получить на своих смартфонах видеографы. Ухищрений, которые по итогу делают менее ресурсоемкие форматы записи видео на смартфоне даже более эффективными и качественными, чем RAW.

Да, будет интересно!

А еще для демонстрации и подтверждения общих положений я опубликую рабочий код на языке Java. Чтобы повторить описанное или даже что-то улучшить, вам нужно знать Java и уметь читать документацию Android.

Но сначала представлюсь

Меня зовут Александр Трофимов, я программист и энтузиаст мобильной видеографии. А еще я разработчик уже довольно известного приложения профессиональной видеосъемки для Андроид-смартфонов mcpro24fps.

Что побудило меня написать эту статью?

Прежде всего, желание показать реальное положение вещей. Ведь сами понятия RAW-видео, как и Log-видеоперекочевали на смартфоны из мира «взрослых» и больших камер. А маркетологи приложили все усилия к тому, чтобы пользователь считал, что его смартфон за 300 долларов уже давно снимает лучше профессиональной камеры за 300 тысяч долларов.

В процессе работы над своим приложением я провожу достаточно много времени в изысканиях и экспериментах со смартфонами самого разного класса. А еще я получаю фидбек от тысяч пользователей с их чаяниями и просьбами.

С момента написания предыдущих статей (тык), (тык) и (тык) мне удалось существенно прокачать Log-съемку и все, что с ней связано. В том числе, дать возможность адаптировать Log гамма‑кривые от «взрослых» камер с большими сенсорами под маленькие сенсоры каждого конкретного смартфона.

И вот, казалось бы, эффективность Log на смартфоне доведена до предела, но..

ВСЕ ХОТЯТ ещё и RAW!

Почему? А потому что:
«У меня вообще‑то флагман, я деньги заплатил!»
«А у меня восемнадцать ядер в телефоне, внешний аккумулятор и карта памяти внешняя — должен всё тянуть!»
Ну и так далее..

Говоря о самых мощных процессорах, стопитсотмегапиксельных сенсорах многие как‑то забывают об ограничениях наших с вами компактных Андроид‑смартфонов. Ограничениях, которые мы и разберем подробно далее..

Что же такое RAW?

И начнем мы с самого простого — определения того, как выглядит RAW‑видео на взрослых камерах, какие стандарты существуют. И здесь негде разбежаться. Их всего два:

  1. Старый добрый ZIP с бесконечным количеством кадров в формате DNG.

  2. MXF контейнер, с метаданными CinemaDNG и «колбасой» данных в формате DNG. Преимущество этого формата заключается в том, что здесь можно указать много всяких метаданных, включая скорость кадров, которые понимают монтажные программы.

Всё. Остальные подходы — это костыли и уловки, к которым индустрия видео не приучена. А значит, монтажные приложения не будут это поддерживать.

Погружаемся в код

Сейчас будет много кода и пояснений к нему. Те, кто не очень умеет в код, могут сразу переходить к разделу с практическими экспериментами!

Очень хотелось бы воспользоваться контейнером MXF, но, похоже, для этого придется писать свой Muxer, поддерживающий этот контейнер. Для этого надо прочитать документацию, понять ее, и правильно реализовать. На это надо достаточно много времени, а ниже мы поймем, что и это нас бы не спасло (хотя надежда остается до момента ее «убийства»). Отбрасываем этот вариант и возвращаемся к классике, которой пользуются разные видео‑камеры среднего ценового сегмента.

И так, наша задача выглядит проще некуда. Разложим ее по шагам.

  1. Взять сырые данные с сенсора камеры.

  2. Сформировать на их основе файл DNG.

  3. Положить DNG в ZIP.

Шаг первый.

Запускаем сессию захвата, где хотя бы одна поверхность (Surface) настроена на RAW_SENSOR. RAW_SENSOR есть почти у всех, и этот 16-битный формат сразу готов для работы с нативным DNGCreator. Можно не заморачиваться о том, сколько бит выдает сенсор. Для того, чтобы сессия захвата могла сконфигурировать нужную нам поверхность, мы будет использовать ImageReader.

rawImageReader = ImageReader.newInstance(rawResolution.getWidth(),
    rawResolution.getHeight(), ImageFormat.RAW_SENSOR, 2);

rawResolution это поддерживаемое разрешение, взятое из системной информации.
Для размера буфера взято всего 2 кадра, потому что нас не интересует отсрочка проблемы производительности. Если есть проблема, мы хотим ее увидеть сразу.
ImageReader готов, теперь надо добавить OnImageAvailableListener, чтобы получать кадры и иметь возможность обработать их. Сначала самый простой вариант:

ImageReader.OnImageAvailableListener listener = r -> {
  Image i = null;
  if (!RECORDING_STARTED) {
    try {
      i = r.acquireLatestImage();
    } finally {
      if (i != null)
        try {
          i.close();
        } finally {
          i = null;
        }
    }
    return;
  }
  try {
    i = r.acquireNextImage();
  } catch (IllegalStateException e) {
    e.printStackTrace();
    i = null;
  } finally {
    if (i != null)
      try {
        i.close();
      } finally {
        i = null;
      }
  }
};
mRAWImageReader.setOnImageAvailableListener(listener, handler);

Пока запись не началась, мы, чтоб не тратить ресурсы, из всего буфера выбираем только самый последний кадр. Как только начинается запись, мы начинаем считывать каждый кадр. Теперь i это наш кадр, нам надо его обработать и сохранить.

Шаг второй.

DngCreator dngCreator = new DngCreator(cameraCharacteristics, captureResult);
dngCreator.writeImage(outputStream, i);

Создаем DNGCreator на основе характеристик камеры и результата захвата. Но… откуда у нас результат захвата? А ни от куда. У нас его нет, мы должны его получить, где-то временно сохранить, и выдать его при создании файла DNG.

Как это сделать? Очевидно, нужен кеш. Для этого мы будем использовать LruCache, где Long это таймкод кадра, а CaptureResult результат захвата в onCaptureCompleted в функции обратного вызова сессии захвата. LruCache это кеш, который имеет ограниченное количество элементов, что защищает нас от утечки памяти.

if (RECORDING_STARTED) {
  rawTime = result.get(CaptureResult.SENSOR_TIMESTAMP);
  if (rawTime != null) {
    сaptureResultsCache.put(rawTime, result);
  }
}

Сохраняем в кеш только, если начата запись.
Теперь достаточно обратиться к кешу и получить нужную нам запись.

long timestamp = i.getTimestamp();
captureResult = captureResultsCache.get(timestamp);

Казалось бы, вот оно, осталось только сохранить/упаковать в ZIP готовый файл. Но нет. У нас две проблемы:

  1. CaptureResult в onCaptureCompleted приходит позже, чем приходит кадр в OnImageAvailableListener.

  2. DNG может создаваться так долго, что мешает сессии захвата, опуская скорость кадров почти до нуля и в конце концов вешая приложение.

Чтобы решить первую проблему, нам, очевидно, нужен кеш для кадров, из которого мы будем пытаться получить кадр при получении CaptureResult в onCaptureCompleted. Для кеша мы можем использовать LruCache, но этим мы только усугубим проблему 2, потому что Image, а с ним и буфер кадра, будет заблокирован до момент, пока не будет прочитан из кеша и закрыт. Поэтому мы пойдем путем решения обеих проблем одновременно. Перво-наперво нам надо как можно быстрее освободить Image, закрыть его. Также всю обработку DNG надо вынести в отдельную ветку. Для этого мы будем получать ByteBuffer из Image, конвертировать его в byte[], и сохранять в кеш, тут же освобождая Image через close(); Для того, чтобы в кеш можно было сохранить дополнительные данные размера кадра и CaptureResult (так будет удобней), мы создаем свой класс объекта DngPacket.

public class DngPacket {
  final byte[] dngData;
  final Size size;
  final long timestamp;
  CaptureResult result = null;
  DngPacket(byte[] dngData, Size size, long timestamp) {
    this.dngData = dngData;
    this.timestamp = timestamp;
    this.size = size;
  }
  DngPacket(byte[] dngData, Size size, long timestamp, CaptureResult result) {
    this.dngData = dngData;
    this.timestamp = timestamp;
    this.size = size;
    this.result = result;
  }
}

Для отдельных веток мы используем ExecutorService mExecutor и execute();

После того, как мы добавим обработку DNG и кеш, мы обнаружим, что стало очень неудобно вызывать обработку DNG и последующее сохранение его в ZIP. Поэтому мы добавляем очередь с фиксированным количеством элементов, в которой будет происходить создание DNG — LinkedBlockingQueue dngQueue;
На старте записи мы определяем количество элементов.

dngQueue = new LinkedBlockingQueue<>(4);

И создаем ветку, в которой эта очередь будет читаться и обрабатываться.

dngWriterThread = new Thread(() -> {
  try {
    while (RECORDING_STARTED || dngQueue.isEmpty()) {
      DngPacket packetOriginal = null;
      try {
        packetOriginal = dngQueue.poll(300, TimeUnit.MILLISECONDS);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        break;
      }
      if (packetOriginal != null) {
        final DngPacket packet = packetOriginal;
        if (mExecutor != null && !mExecutor.isShutdown()) {
          mExecutor.execute(() -> {
            try {
              ByteArrayOutputStream byteArrayOutputStream =
                  new ByteArrayOutputStream();
              DngCreator dngCreator =
                  new DngCreator(mCameraCharacteristics, packet.result);
              dngCreator.writeByteBuffer(byteArrayOutputStream, packet.size,
                  ByteBuffer.wrap(packet.dngData), 0);
              try {
                dngCreator.close();
              } finally {
                //
              }
              byte[] dngBytes = byteArrayOutputStream.toByteArray();
              try {
                zipQueue.offer(new DngZipPacket(dngBytes,
                    packet.timestamp + ".dng")); // да, здесь снова очередь, но
                                                 // теперь для сохранения в ZIP.
              } catch (Exception e) {
              }
            } catch (IOException e) {
            }
          });
        }
      }
      if (!RECORDING_STARTED && dngQueue.isEmpty()) {
        dngQueue.clear();
        break;
      }
    }
  } finally {
    //
  }
}, "DNGWriterThread");
dngWriterThread.start();

Здесь мы снова используем mExecutor, чтобы создание DNG происходило в отдельной ветке и не блокировало ветку получения данных из очереди.

Еще наблюдательные могут заметить новый объект DngZipPacket. Это тоже отдельный класс для более простой передачи в очередь и последующего чтения.

public class DngZipPacket {
  final byte[] dngData;
  final String entryName;
  DngZipPacket(byte[] dngData, String entryName) {
    this.dngData = dngData;
    this.entryName = entryName;
  }
}

В результате получаем такой setOnImageAvailableListener.

ImageReader.OnImageAvailableListener listener = r -> {
  Image i = null;
  if (!RECORDING_STARTED) {
    try {
      i = r.acquireLatestImage();
    } finally {
      if (i != null)
        try {
          i.close();
        } finally {
          i = null;
        }
    }
    return;
  }
  if (zipOutputStream == null) {
    stopRAWRecording(); // функция для остановки записи
    return;
  }
  try {
    i = r.acquireNextImage();
  } catch (IllegalStateException e) {
    e.printStackTrace();
    i = null;
    return;
  }
  if (i == null || i.getFormat() != ImageFormat.RAW_SENSOR) {
    return;
  }
  final Image rawImage = i;
  if (mExecutor != null && !mExecutor.isShutdown()) {
    mExecutor.execute(() -> {
      try {
        if (сameraCharacteristics == null) {
          rawImage.close();
          return;
        }
        long timestamp = rawImage.getTimestamp();
        Size size = new Size(rawImage.getWidth(), rawImage.getHeight());
        byte[] bytes =
            new byte[rawImage.getPlanes()[0].getBuffer().remaining()];
        rawImage.getPlanes()[0].getBuffer().get(bytes);
        try {
          rawImage.close();
        } finally {
          //
        }
        CaptureResult captureResult = captureResultsCache.get(timestamp);
        if (captureResult == null) {
          // если не находим CaptureResult, сохраняем кадр в кеш
          mDNGCache.put(timestamp, new DngPacket(bytes, size, timestamp));
          return;
        }
        dngQueue.offer(new DngPacket(bytes, size, timestamp, captureResult))
      } catch (Exception e) {
        e.printStackTrace();
      }
    });
  }
};

Отдельное внимание хочу обратить на dngQueue.offer. Мы используем offer вместо put, потому что put ждет, пока освободится место в очереди, чем блокирует функцию. offer пытается вставить элемент в очередь, но если места нет, просто откидывает его. Нам нет смысла пытаться впихнуть все. Если производительности не хватает, то так тому и быть.

Кеш для DNG без CaptureResult выглядит так.

LruCache<Long, DngPacket> mDNGCache;
mDNGCache = new LruCache<>(4);

Все RAW буферы, у которых нашлись данные CaptureResult в кеше captureResultsCache отправляются в очередь на создание DNG и последующее сохранение в ZIP.

Теперь мы вспоминаем, для чего вообще нам был нужен кеш для DNG. Для того, чтобы реагировать в ситуации, когда CaptureResult приходит позже кадра. Для этого мы редактируем код в onCaptureCompleted.

if (RECORDING_STARTED) {
  rawTime = result.get(CaptureResult.SENSOR_TIMESTAMP);
  if (rawTime != null) {
    DngPacket packet = mDNGCache.get(rawTime);
    if (packet != null) {
      packet.result = result;
      try {
        dngQueue.offer(packet);
      } catch (Exception e) {
        //
      }
      mDNGCache.remove(rawTime);
    } else {
      captureResultsCache.put(rawTime, result);
    }
  }
}

На этом второй шаг завершается. У нас получилось временно сохранить RAW-данные, данные CaptureResult и создать DNG file, не мешая сессии работать.

Мы обрабатываем и отправляем в ZIP DNG-файлы в произвольном порядке. Нас не интересует в каком порядке они выходят после обработки. Архив перед употреблением в монтажной программе, будет распакован, файлы будут отсортированы по названиям. Поэтому на начальном этапе, для тестов, нам достаточно вписать таймкод в название.

Шаг третий.

Нам осталось запустить процесс складывания файлов в ZIP. Для этого мы создаем очередь для DNG файлов и отдельную ветку, в которой файлы будут подготавливаться и сохраняться в ZIP.

LinkedBlockingQueue zipQueue;
zipQueue = new LinkedBlockingQueue<>(4);

Для работы с ZIP мы используем ZipOutputStream zipOutputStream.

// открываем стрим файла
OutputStream stream = resolver.openOutputStream(fileUri);
// оборачиваем его в BufferStream, чтобы данные скидывались не сразу, а
// чуть-чуть накапливались
FileOutputStream bos = new BufferedOutputStream(stream);
// оборачиваем в BufferStream в ZipOutputStream
zipOutputStream = new ZipOutputStream(bos);
// Настройки, чтобы не происходило сжатия
zipOutputStream.setMethod(ZipOutputStream.STORED);
zipOutputStream.setLevel(Deflater.NO_COMPRESSION);

А дальше запускаем ветку для работы с ZipOutputStream.

zipWriterThread = new Thread(() -> {
  try {
    while (RECORDING_STARTED || !zipQueue.isEmpty()) {
      DngZipPacket packet = null;
      try {
        packet = zipQueue.poll(300, TimeUnit.MILLISECONDS);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        break;
      }
      if (packet != null) {
        try {
          ZipEntry entry = new ZipEntry(packet.entryName);
          entry.setSize(packet.dngData.length);
          entry.setCompressedSize(packet.dngData.length);
          CRC32 crc = new CRC32();
          crc.update(packet.dngData);
          entry.setCrc(crc.getValue());
          zipOutputStream.putNextEntry(entry);
          zipOutputStream.write(packet.dngData, 0, (int) packet.dngData.length);
          zipOutputStream.closeEntry();
        } catch (IOException e) {
        }
      }
      if (!RECORDING_STARTED && zipQueue.isEmpty()) {
        break;
      }
    }
  } finally {
    try {
      if (zipOutputStream != null) {
        zipOutputStream.finish();
        zipOutputStream.close();
      }
    } catch (IOException e) {
    }
    zipOutputStream = null;
  }
}, "ZipWriterThread");
zipWriterThread.start();

Обращаю внимание, что мы для сохранения в ZIP не используем многопоточность, чтобы не было соревнования за создание записей. Каждый пакет должен быть сохранен отдельно только со своими данными. Нельзя, чтобы после putNextEntry туда вписалось несколько DNG-файлов.

Выше мы установили настройки «без сжатия», потому что это самые легкие настройки для процессора. Для начала нам и этого достаточно. А потом окажется, что это единственно возможное.

Вот и весь механизм записи RAW-видео, как это делают «взрослые» камеры. Давайте повторим его уже на устройствах.

А теперь непосредственно к опытам!

Для проведения опытов я взял несколько достаточно свежих и мощных смартфонов на OC Android: Samsung S24 Ultra, Xiaomi 14 Ultra, Sony Xperia 5 mk IV и Samsung S25 Ultra.

Размер одного RAW буфера - это 24-25 Мегабайт при примерном разрешении сенсора 12 Мп.

Запускаем эксперимент и что мы видим:

  • Samsung S24 Ultra не справляется с задачей даже при скорости 24 к/с.

  • Xiaomi 14 Ultra при скорости 24 к/с не справляется вовсе (хотя под “справляется” мы даже допускаем наличие нескольких выпавших кадров).

  • Sony Xperia 5 IV вообще роняет скорость до 10-13 кадров в секунду, т.е. тоже не справляется.

Напомню, эти девайсы уж точно не назовешь слабыми. Но они не справляются с записью RAW-видео.

А теперь давайте посмотрим более детально на то, как в моем тесте проявил себя один из флагманов этого года — Samsung S25 Ultra.

Начинается все просто прекрасно, однако когда размер файла приближается к 5 - 8 Гб, что в эквиваленте всего 10 секунд записи, флагманская карета превращается в тыкву.

На старте:

  • создание DNG 20-25 мс

  • запись в ZIP-файл 10-15 мс

Уже видно, что суммарно весь процесс занимает больше 33 мс, необходимых для бесперебойной работы. Дальше происходит накопительный эффект, и время обработки существенно меняется:

  • создание DNG 40-50 мс

  • запись в ZIP-файл 10-15 мс.

Выводы из опытов

Опыты показали, что сжать файл на лету задача сложная, и с ней не справился ни один из испытуемых аппаратов. И если даже премиальные флагманы из мира Андроид не справляются с этой задачей, очевидно, что Андроид все еще не готов к съемке RAW‑видео.

А как же оптимизация (костылизация)?

Была у меня мысль о том, что некоторые девайсы умеют выдавать RAW10, и это было бы неплохим подспорьем для оптимизации процесса и позволило бы существенно снизить размер одного кадра: с 25 Мб до 15 Мб. Но оказалось, что нативный DNGCreator работает только с 16-битным RAW_SENSOR.

В остальном же методы оптимизации очевидны.

Сначала мы должны срезать пустые биты, т.к. большинство сенсоров у нас 10-битные, то 6 старших бит можно отрезать.

В случае, если система поддерживает RAW12, я бы задумался о том, чтобы отрезать только 4 бита.

Следующий шаг — это использование кропа. Можно уменьшить кадр или усреднить через складывание значений и получить на выходе 1080p вместо 2160p. Но для всего этого придется придумывать свой контейнер, который впоследствии должен иметь свой распаковщик для Windows и Mac.

Но всё это усложняет задачу в разы, а заодно и подводит нас к еще одному важному выводу:

Трудозатраты на оптимизацию и та разница в качестве, которую дает RAW в сравнении с правильно снятым YUV (h265/h264), не говорят в пользу RAW.

А если вспомнить, что мы все же говорим о мобильных устройствах, в которых крайне важным ресурсом являются и место на диске, и расход батареи, да и возможность снимать на Андроид‑смартфоны не за все деньги мира — то съемка в Log гамма‑кривых оказывается интереснее во всех отношениях.

Доверяем, но проверяем!

В свежем обновлении видеокамеры mcpro24fps я решил открыть доступ к данной функции в режиме Лаборатории для всех пользователей. За 2 месяца работы над RAW-видео параллельно написанию статьи мне удалось внедрить некоторые оптимизации и улучшения в код. Все, кто не хочет писать свое собственное приложение, могут самостоятельно активировать Запись RAW и провести все эксперименты прямо на своем Андроид смартфоне. Правда, для этого придется поддержать нас покупкой приложения, к примеру, в Google Play или RuStore.

Важно понимать: работоспособность всех функций в режиме Лаборатории не гарантирована. Со временем они могут исчезнуть из приложения, видоизмениться или же потребовать дополнительной оплаты (In-app покупки, подписка). Активируете их исключительно под вашу ответственность.

Вместо послесловия

Спасибо, что дочитали! В комментариях я готов прочитать всё, что только придет вам в голову: от критики моего мнения, до каких-либо решений, касаемо оптимизации записи RAW-видео. Не обещаю, что ваши предложения и идеи будут использованы в дальнейшем, но они могут навести на интересные мысли.

Комментарии (0)


  1. sic
    18.09.2025 13:07

    Ну подождите, вы сознательно отошли от использования многопотока (по для меня не очень убедительным причинам), а такое ощущение, что с ним все было бы на этих аппаратах просто отлично.


    1. mcpro Автор
      18.09.2025 13:07

      В том месте, где приходит буфер кадра, ничего не сделать и многопоток ничего не решает. Кадр находится в DirectByteBuffer и его задержка переполняет буфер сессии. И пока он не будет скопирован, его не отпустить. Следующий момент касается помещения в ZIP. А там важно открыть запись позиции поместить файл и закрыть запись позиции. Многопоток ломает файл. Контролировать многопоток это то же самое, что его не использовать.


      1. sic
        18.09.2025 13:07

        Каждый DirectByteBuffer скопировать (по значению, конечно) в промежуточный пул буферов, их раздать воркерам конвертирования в dng, их результаты уже через синхронизацию заливать в zip поток. Если основное время - конверсия в dng, то это должно сработать.

        Насколько я помню, копирование 50 мегабайт памяти - порядка пары миллисекунд в худшем случае.


        1. mcpro Автор
          18.09.2025 13:07

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

          Копирование в нативной области действительно занимает очень мало. А вот в области Java поболее будет, чем пара миллисекунд.


  1. JerryI
    18.09.2025 13:07

    А как, кстати, эти проблемы решены в айфонах? Никак?


    1. mcpro Автор
      18.09.2025 13:07

      Нативной поддержкой от производителя. В новых айфонах будет ProRes RAW.


  1. PrivateStaticFinal
    18.09.2025 13:07

    А как это было сделано в blackmagic pocket самого первого поколения? Там мощнее процессор стоял?


    1. mcpro Автор
      18.09.2025 13:07

      Там скорее всего это всё происходило на уровне ISP, т.е. отдельный модуль на процессоре, который занимается только обработкой картинки, включая сжатие, и надо было только успевать в ZIP складывать. На Андроиде мы получаем RAW-кадр полного разрешения и нам надо сформировать файл еще со всеми тегами. А если надо уменьшить нагрузку на накопитель, то надо с этим кадром что-то сделать, и делать приходится на центральном процессоре или ГПУ, что совсем другое по производительности, нагреву и потреблению ресурсов.
      Если бы был отдельно доступен ISP, я думаю, работало бы много шустрее всё.


      1. PrivateStaticFinal
        18.09.2025 13:07

        А как это сделано в magic lantern: конвертирование в dng уже на компьютере?


        1. mcpro Автор
          18.09.2025 13:07

          Как на андроиде. Поэтому это больше похоже на страдания, чем на съемку RAW.


  1. IZh
    18.09.2025 13:07

    У вас тут получается поток данных 25 МБ * 24 кадра = 600 МБ/с. Тут нужно, по возможности, делать zero copy, то есть уменьшать копирование данных из буфера в буфер.

    Я детально не разбирался, но выглядит так, что создание того-же DNG может состоять из приклеивания фиксированного заголовка (и хвоста, если требуется), так как все кадры одинаковы, а не изготовление заголовка с нуля для каждого кадра.

    Аналогично с непожатым ZIP-архивом. Все кадры вместе с заголовками должны иметь одинаковый размер, а потому писать их можно и параллельно. А в конце доклеить структуру со списком файлов.

    Я бы для эксперимента попробовал бы просто писать сырые данные в файл из нескольких потоков параллельно — без заголовков DNG и ZIP («колбаса» кадров). Просто, чтобы убедиться, что такой поток данных успевает обрабатываться.