Додаємо водяні знаки до файлів за допомогою AWS Lambda

Я інженер-програміст із понад 4 роками досвіду у сфері розробки програмного забезпечення. Брав участь у реалізації проєктів, пов’язаних з обробкою мультимедійних даних, автоматизацією бізнес-процесів та впровадженням хмарних рішень на базі AWS та Microsoft Azure. Основні напрямки експертизи: розробка бекенд-систем (бази даних, бізнес-логіка), інтеграція з хмарними сервісами та забезпечення інформаційної безпеки. І у своїй практиці часто зустрічав питання, яке хочу розглянути.

Впровадження водяних знаків у файли є ключовим аспектом захисту прав інтелектуальної власності та забезпечення автентичності контенту. У даній статті розглянемо методологію додавання водяних знаків до різноманітних типів файлів, таких як відео, зображення та pdf-документи за допомогою сервіса AWS Lambda. Окрім того, буде здійснено аналіз потенційних ризиків, пов’язаних з обмеженнями часу виконання (timeout) у AWS Lambda, та запропоновано шляхи їх уникнення.

Ключові компоненти

Для досягнення поставленої мети будуть використані наступні складові:

  • AWS Lambda для обробки файлів.
  • AWS S3 для зберігання файлів.
  • ffmpeg для обробки відео.
  • Jimp для обробки зображень.
  • pdf-lib для обробки PDF-документів.
  • fs для взаємодії з файловою системою і локальним сховищем.
  • aws-sdk для спрощення розробки.

Увага! Для використання ffmpeg у AWS Lambda необхідно додати Lambda Layer, який містить ffmpeg. Lambda Layers дозволяють ділитися бібліотеками та іншими залежностями між різними функціями Lambda.

Для виклику нашої функції можна використати AWS SQS або налаштувати тригери для S3 Bucket.

Підбирання кодеків для відео

Для обробки відео ми будемо використовувати ffmpeg. Вибір кодека залежить від формату відео. Ось приклад функції для підбирання кодека:

const chooseBestCodec = (inputPath) => {
  const extname = path.extname(inputPath).toLowerCase();
  
  switch (extname) {
    case '.mp4':
      return 'libx264'; // H.264 - best for .mp4 files
    case '.avi':
      return 'mpeg2video'; // For .avi files
    case '.mov':
      return 'libx264'; // For .mov files
    case '.webm':
      return 'libvpx-vp9'; // VP9 - optimal for .webm files
    case '.mkv':
      return 'libx264'; // H.264 works well for .mkv
    default:
      return 'libx264'; // Default fallback
  }
};

Оптимальність кодека у функції chooseBestCodec визначається на основі відповідності кодека до формату відео файлу. Ось основні принципи, за якими обираються кодеки:

1. Сумісність з форматом файлу. Кожен формат відео файлу має свої оптимальні кодеки, які забезпечують найкращу якість та ефективність стиснення.

  • Для mp4-файлів використовується «libx264», оскільки H.264 (libx264) є стандартним кодеком для цього формату і забезпечує високу якість при відносно невеликому розмірі файлу.
  • Для avi-файлів використовується «mpeg2video», оскільки MPEG-2 є одним з традиційних кодеків для цього формату.
  • Для mov-файлів також використовується «libx264», оскільки цей кодек добре підтримується форматом QuickTime.
  • Для webm-файлів використовується «libvpx-vp9», оскільки VP9 є стандартним кодеком для WebM і забезпечує високу якість при ефективному стисненні.

2. Універсальність та підтримка. Якщо формат файлу не визначений або не підтримується специфічним кодеком, використовується «libx264» як універсальний кодек, який підтримується більшістю відеоплеєрів і платформ.

Таким чином вибір кодека базується на забезпеченні найкращої якості відео та сумісності з форматом файлу.

Тут описано основні функції обробники відповідно до типу файлу.

Додавання водяного знаку до відео

Для додавання водяного знаку до відео ми будемо використовувати ffmpeg. Ось приклад функції:

const addWatermarkToVideo = async (inputPath, outputPath, watermarkPath) => {
 ffmpeg.setFfmpegPath(ffmpegPath);

 console.log('Started processing video:', inputPath);

 try {
   const codec = chooseBestCodec(inputPath);
   console.log('Selected codec:', codec);

   return new Promise((resolve, reject) => {
     ffmpeg(inputPath)
       .input(watermarkPath)
       .complexFilter([
         {
           filter: 'overlay',
           options: { x: '(main_w-overlay_w)/2', y: '(main_h-overlay_h)/2' }, // Center watermark
         },
       ])
       .output(outputPath)
       .videoCodec(codec)
       .on('start', (commandLine) => {
         console.log('Running ffmpeg command:', commandLine);
       })
       .on('progress', (progress) => {
         if (progress.percent) {
           console.log(`Processing: ${Math.floor(progress.percent)}% done`);
         } else {
           console.log('Processing:', progress);
         }
       })
       .on('end', () => {
         console.log('Watermark added successfully to video:', outputPath);
         resolve();
       })
       .on('error', (err, stdout, stderr) => {
         console.error('Error occurred while adding watermark:', err);
         console.error('ffmpeg stdout:', stdout);
         console.error('ffmpeg stderr:', stderr);
         reject(new Error(`ffmpeg error: ${err.message}`));
       })
       .run();
   });
 } catch (err) {
   console.error('Unexpected error in addWatermarkToVideo:', err);
   throw err;
 }
};

Додавання водяного знаку до зображень

Для обробки зображень ми будемо використовувати Jimp. Ось приклад функції:

const addWatermarkToImage = async (
 inputPath,
 outputPath,
 watermarkPngBuffer
) => {
 const image = await Jimp.read(inputPath);

 console.log('Image read:', inputPath);

 const watermark = await Jimp.read(watermarkPngBuffer);

 console.log('Watermark png buffer read');

 watermark.resize({ w: image.bitmap.width / 4, h: Jimp.AUTO });

 console.log('Watermark resized');

 const x = Math.floor((image.bitmap.width - watermark.bitmap.width) / 2);
 const y = Math.floor((image.bitmap.height - watermark.bitmap.height) / 2);

 console.log('Watermark position:', x, y);
 watermark.opacity(1);

 console.log('Watermark opacity set to 1');

 image.composite(watermark, x, y, {
   mode: BlendMode.SRC_OVER,
 });

 console.log('Watermark composited');

 await image.write(outputPath);

 console.log('Image watermarked:', outputPath);
};

Додавання водяного знаку до PDF-документів

Для обробки PDF-документів ми будемо використовувати pdf-lib. Ось приклад функції:

const addWatermarkToDocument = async (
 inputPath,
 outputPath,
 watermarkPngBuffer
) => {
 console.log('Processing PDF:', inputPath);

 const existingPdfBytes = fs.readFileSync(inputPath);

 const pdfDoc = await PDFDocument.load(existingPdfBytes);
 const pages = pdfDoc.getPages();
 for (const page of pages) {
   const { width, height } = page.getSize();

   const pngImage = await pdfDoc.embedPng(watermarkPngBuffer);

   page.drawImage(pngImage, {
     x: (width - pngImage.width) / 2,
     y: (height - pngImage.height) / 2,
     width: pngImage.width,
     height: pngImage.height,
     contrast: 1,
     opacity: 1,
     blendMode: 'Exclusion',
   });
 }

 const pdfBytes = await pdfDoc.save();

 fs.writeFileSync(outputPath, pdfBytes);

 console.log('PDF watermarked:', outputPath);
};

Обробка файлів у AWS Lambda

Функція handler обробляє події AWS Lambda, додаючи водяні знаки до файлів (відео, зображень, PDF документів) та завантажуючи їх назад до S3. Ось покроковий опис того, що відбувається у цій функції:

1. Парсимо тіло події (Event).
2. Отримуємо записи (Records) з події, робимо перевірку на наявність записів та їх формат.
3. Отримуємо шляхи до файлів із водяними знаками.
4. Визначення тимчасових шляхів для водяних знаків.

Так, у AWS Lambda доступна папка tmp для зберігання тимчасових файлів. Ось деякі важливі моменти щодо використання цієї папки:

  • Розмір. Папка tmp за замовчуванням має обмеження пам’яті у 512 МБ. Це означає, що ви можете зберегти тимчасові файли загальним розміром до 512 МБ.
  • Тривалість зберігання/ Файли, збережені у папці tmp, доступні лише протягом виконання функції Lambda. Після завершення виконання функції файли можуть бути видалені.
  • Спільний доступ між викликами. Якщо функція Lambda викликається повторно на тому ж самому контейнері, файли у папці tmp можуть залишатися доступними між викликами. Однак не слід покладатися на це, оскільки AWS може видалити ці файли або перезапустити контейнер у будь-який момент.

Використання папки tmp у вашій функції Lambda дозволяє зберегти тимчасові файли, необхідні для обробки, такі як завантажені файли з S3 або тимчасові файли водяних знаків.

Це дозволяє функції обробляти файли локально перед завантаженням результатів назад до S3.

 const tempWatermarkSVGFilePath = `/tmp/watermark-${path.basename(watermarkPath)}`;
 const tempWatermarkPNGFilePath = `/tmp/watermark-${path.basename(watermarkPngPath)}`;

5. Обробка кожного запису:

for (const record of records) {
  const messageBody = JSON.parse(record.body);
  const keyUrl = new URL(messageBody.result);
  const key = keyUrl.pathname.substring(1);
  const bucket = process.env.S3_BUCKET_NAME;
  const extension = path.extname(key);
  const originalFilePath = `/tmp/${path.basename(key)}`;
  const watermarkedFilePath = `/tmp/watermarked-${path.basename(key)}`;

6. Завантаження водяного знаку.

Для відеоформатів використовуємо формат .png для інших типів файлів .svg, що забезпечує кращу сумісність.

let response;
     if (
       key.endsWith('.mp4') ||
       key.endsWith('.mov') ||
       key.endsWith('.avi')
     ) {
       response = await axios.get(watermarkPngPath, {
         responseType: 'arraybuffer',
       });
     } else {
       response = await axios.get(watermarkPath, {
         responseType: 'arraybuffer',
       });
     }

     const watermarkBuffer = Buffer.from(response.data);
     const watermarkPngBuffer = await sharp(watermarkBuffer).png().toBuffer();

     fs.writeFileSync(tempWatermarkPNGFilePath, response.data);

7. Отримання оригінального файлу з S3:

     const originalFile = await s3
       .getObject({ Bucket: bucket, Key: key })
       .promise();

     fs.writeFileSync(originalFilePath, originalFile.Body);

8. Обробка файлу в залежності від його типу.

На основі типу файлу обираємо відповідну функцію обробник, які описано вище.

9. Завантаження файлу з водяним знаком до S3:

     const watermarkedFile = fs.readFileSync(watermarkedFilePath);

     let watermarkedKey = key.replace(extension, `-watermarked${extension}`);
     watermarkedKey = watermarkedKey.replace('uploads/', 'watermarked/');

     await s3
       .putObject({
         Bucket: bucket,
         Key: watermarkedKey,
         Body: watermarkedFile,
       })
       .promise();

     console.log('Watermarked file uploaded:', watermarkedKey);

10. Видалення тимчасових файлів:

     fs.unlinkSync(originalFilePath);
     fs.unlinkSync(watermarkedFilePath);
     fs.unlinkSync(tempWatermarkPNGFilePath);

11. (Опціонально) можна оновити адресу на файл у базі даних або видалити оригінальний файл.
12. (Опціонально) можна автоматизувати створення вотермарки на завантаження файлу в S3 (docs.aws.amazon.com/...​/dg/with-s3-tutorial.html).

Ризики AWS Lambda

AWS Lambda має обмеження на час виконання функцій, яке за замовчуванням становить 3 секунди, але може бути збільшене до 15 хвилин. Якщо обробка файлів займає більше часу, ніж дозволено, функція може завершитися з помилкою timeout. Щоб уникнути цього, можна використовувати наступні підходи:

  • Розділити обробку великих файлів на менші частини.
  • Використовувати AWS Step Functions для оркестрації довготривалих завдань.
  • Оптимізувати код для зменшення часу обробки.
  • Збільшити ресурси Лямбди.

AWS Lambda має кілька обмежень, які важливо враховувати при розробці функцій, щоб забезпечити їх ефективну і стабільну роботу.

Ось основні обмеження AWS Lambda, які важливо враховувати під час розробки функцій:

  1. Час виконання:
    • За замовчуванням функції Lambda мають обмеження на час виконання у 3 секунди.
    • Це обмеження може бути збільшене до максимум 15 хвилин.
  2. Розмір пакету розгортання:
    • Максимальний розмір пакету розгортання (включаючи всі залежності) становить 50 МБ під час завантаження через консоль або CLI.
    • Якщо використовуються Lambda Layers, загальний розмір усіх шарів та функції не повинен перевищувати 250 МБ.
  3. Пам’ять:
    • Функції Lambda можуть бути налаштовані на використання від 128 МБ до 10 ГБ пам’яті.
    • Кількість виділеної пам’яті також впливає на кількість доступних обчислювальних ресурсів (vCPU).
  4. Тимчасове сховище:
    • Кожна функція Lambda має доступ до 512 МБ тимчасового сховища у tmp директорії.
    • Це сховище використовується для зберігання тимчасових файлів під час виконання функції.
  5. Мережеві з’єднання:
    • Функції Lambda можуть здійснювати вихідні мережеві з’єднання, але не можуть приймати вхідні з’єднання.
    • Для доступу до ресурсів у приватній VPC потрібно налаштувати VPC конфігурацію для функції.
  6. Обмеження на кількість викликів:
    • AWS Lambda має обмеження на кількість одночасних викликів функцій (concurrent executions). За замовчуванням це обмеження становить 1000 одночасних викликів на обліковий запис.
    • Це обмеження може бути збільшене за запитом до AWS Support.
  7. Розмір вхідних та вихідних даних:
    • Максимальний розмір вхідних даних (payload) для синхронних викликів становить 6 МБ.
    • Максимальний розмір вхідних даних для асинхронних викликів становить 256 КБ.

Ці обмеження важливо враховувати при розробці функцій Lambda, щоб забезпечити їх ефективну та стабільну роботу.

Висновок

Додавання водяних знаків до файлів у AWS Lambda є ефективним способом захисту контенту. Використовуючи ffmpeg, Jimp та pdf-lib, ми можемо обробляти відео, зображення та PDF документи. Важливо враховувати ризики timeout AWS Lambda та оптимізувати код для зменшення часу обробки.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному1
LinkedIn
Ctrl + Enter
Ctrl + Enter

Комент в підтримку нашого проактивного колеги! Дякую за статтю!

Той випадок, коли технічні деталі справді стають частиною загальної картини.
Класно, коли така експертиза є всередині команди!

Я вибачаюсь, але код в прикладах — таке спагетті, за яке зазвичай ще джуни ляща отримують. Ну куди ж таке на люди виносити...

Спеціально залишив такий вид і усі console.log(). Таким чином його дуже зручно дебажити. Якщо хтось захоче перевикористати цей код то може сміло брати цю логікуі змінювати як зручно. Знову ж таки це всього лиш робочий приклад. Фокус на тому, з якими обмеженнями можна стикнутись при розробці схожого функціоналу

console.log майже завжди бентежить в останню чергу.

chooseBestCodec

А можна ще простими словами для новачка:

1. Навіщо тут promise? Це не можна зробити звичайною функцією? Навіщо async? Який I/O тут виконується?
2. Навіщо тут try-catch? toLowerCase може не спрацювати?

Ось ChatGPT каже так краще:

function chooseBestCodec(inputPath) {
  const extname = path.extname(inputPath).toLowerCase();

  if (extname === '.mp4') {
    return 'libx264';
  } else if (extname === '.avi') {
    return 'mpeg2video';
  } else if (extname === '.mov') {
    return 'libx264';
  } else if (extname === '.webm') {
    return 'libvpx-vp9';
  } else {
    return 'libx264'; // Default fallback
  }
}

Або:

function chooseBestCodec(inputPath) {
  const extname = path.extname(inputPath).toLowerCase();

  const codecMap = {
    '.mp4': 'libx264',
    '.avi': 'mpeg2video',
    '.mov': 'libx264',
    '.webm': 'libvpx-vp9',
  };

  return codecMap[extname] || 'libx264'; // Default fallback
}
Впровадження водяних знаків у файли є ключовим аспектом захисту прав інтелектуальної власності та забезпечення автентичності контенту

Справді? А я чогось думав, що то п’ятисекунда перешкода на те, щоб вбити в гугл «remove logo ai» і скористатися одним з мільйона сервісів для цього.

Я також вважаю, що — ЕЦП більш надійне рішення, для захисту контенту, але маємо що маємо. Така «перешкода» краща за відсутність будь якої, до того ж порушення авторського права карається законом

Навіщо потрібні лямбди у 2к25, якщо вже було предостатньо кейсів навіть усередині амазона коли вони були неефективними як у плані коштів так і computing resources ?

залежить від навантаження

так лямбди це не про навантаження а про те як швидко ти можеш написати і деплойнути своє рішення, бо у тебе як плюси — вже готова інтеграція з усією інфраструктурою aws, як мінуси — якщо неправильно розрахував навантаження і час виконання — плати більше ніж за on-premise

Наприклад, вам треба виконувати якусь інтенсів таску 10 разів на день по 5 хвилин. Берете план на тачку 16 vCPU/32Gb ram в AWS Lambda / Azure Func. Тримати такий виділений інстанс — трохи напряжно по бабкам. А в серверлесс наче й ок.

16 vCPU/32Gb ram в AWS Lambda

Такої конфігурації AWS Lambda не існує. Там максимум 10 ГБ.

якщо ця таска одна — можливо, якщо це скейлиться на проект середньої складності де багато хто ці таски скедулить, то ec2 може бути кращим рішенням

І ще я хочу нагадати, що година обчислень на AWS Lambda коштує:

$0.0000000533/ms × 1000 ms × 3600 = $0,19 (ARM, 4 GB RAM, 1 CPU)

а година такого самого (навіть більш потужного) клауд-серверу на Hetzner коштує $ 0.0074 (ARM, 4 GB RAM, 2 CPU), тобто в 25 разів дешевше.

Чекаю на коментарі «а як же безпека та масштабування та рядок в резюме про мікросервіси».

* aws.amazon.com/lambda/pricing
* www.hetzner.com/cloud

хочу нагадати, що година обчислень на AWS Lambda коштує

хочу нагадати, що одна з концепцій serverless — платити виключно за дійсно використаний машинний час, саме тому в AWS Lambda біллінг — в ms, а в Hetzner мінімум — hourly rate,
відповідно перше — точно не для постійних обчислень

в AWS Lambda біллінг — в ms

Перекодування відео вимірюється в хвилинах. Знаючі, які «потужні» сервери дають на AWS Serverless, то це будуть години.

тут згоден,
одна справа — документи в pdf-форматі
і зовсім інша — відео, останнє скоріше антипаттер не тільки для serverless але і для обробки на CPU взагалі

Це рішення було під конкретний проект, де відео не довше 60 с. Звичайно, при потребі для довших відео є інші/кращі опції

Дякую за відповідь. Це важлива інформація щодо вимог — тоді багато питань з архітектури знімається.

Порівнювати ціни в вакуумі, за одиницю роботи — така собі історія.
Лямбда, всякі серверлесси, фаргейти і тп можуть бути дешевшими на етапі старту проекту, коли малі навантаження, за рахунок менших трат на розробку. А на більших масштабах, так, лямбда то дурня

за рахунок менших трат на розробку

Так, так, я бачу як у автора через кожний рядок «console.log», скільки годин або днів він витратив, щоб дізнатись про усі ті «Ризики AWS Lambda» на власному досвіді.

Розділити обробку великих файлів на менші частини.

Тобто одна лямбда розділяє, друга додає водяний знак, третя поєднує знову?

Може тоді вже запустити EC2 instance або ECS контейнер?

Підписатись на коментарі