@jjlmoya/utils-streaming 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +2 -3
  6. package/src/tests/seo_component_contract.test.ts +22 -0
  7. package/src/tests/tool_validation.test.ts +17 -17
  8. package/src/tool/videoBitratePlanner/bibliography.astro +6 -0
  9. package/src/tool/videoBitratePlanner/bibliography.ts +12 -0
  10. package/src/tool/videoBitratePlanner/component.astro +122 -0
  11. package/src/tool/videoBitratePlanner/controller.ts +134 -0
  12. package/src/tool/videoBitratePlanner/dom-views.ts +77 -0
  13. package/src/tool/videoBitratePlanner/entry.ts +29 -0
  14. package/src/tool/videoBitratePlanner/evaluator.ts +19 -0
  15. package/src/tool/videoBitratePlanner/i18n/de.ts +39 -0
  16. package/src/tool/videoBitratePlanner/i18n/en.ts +202 -0
  17. package/src/tool/videoBitratePlanner/i18n/es.ts +39 -0
  18. package/src/tool/videoBitratePlanner/i18n/fr.ts +39 -0
  19. package/src/tool/videoBitratePlanner/i18n/id.ts +37 -0
  20. package/src/tool/videoBitratePlanner/i18n/it.ts +37 -0
  21. package/src/tool/videoBitratePlanner/i18n/ja.ts +37 -0
  22. package/src/tool/videoBitratePlanner/i18n/ko.ts +37 -0
  23. package/src/tool/videoBitratePlanner/i18n/nl.ts +37 -0
  24. package/src/tool/videoBitratePlanner/i18n/pl.ts +37 -0
  25. package/src/tool/videoBitratePlanner/i18n/pt.ts +37 -0
  26. package/src/tool/videoBitratePlanner/i18n/ru.ts +37 -0
  27. package/src/tool/videoBitratePlanner/i18n/sv.ts +37 -0
  28. package/src/tool/videoBitratePlanner/i18n/tr.ts +37 -0
  29. package/src/tool/videoBitratePlanner/i18n/zh.ts +37 -0
  30. package/src/tool/videoBitratePlanner/index.ts +13 -0
  31. package/src/tool/videoBitratePlanner/locale-content.ts +72 -0
  32. package/src/tool/videoBitratePlanner/logic.test.ts +50 -0
  33. package/src/tool/videoBitratePlanner/logic.ts +158 -0
  34. package/src/tool/videoBitratePlanner/seo.astro +15 -0
  35. package/src/tool/videoBitratePlanner/storage.ts +31 -0
  36. package/src/tool/videoBitratePlanner/ui.ts +50 -0
  37. package/src/tool/videoBitratePlanner/video-bitrate-storage-planner.css +612 -0
  38. package/src/tools.ts +2 -1
@@ -0,0 +1,37 @@
1
+ import { createLocalizedContent } from '../locale-content';
2
+
3
+ export const content = createLocalizedContent({
4
+ language: 'pt',
5
+ slug: 'calculadora-bitrate-armazenamento-video',
6
+ title: 'Planejador de Bitrate e Armazenamento de Vídeo',
7
+ description: 'Estime o armazenamento de vídeo, o tempo por fotograma e níveis práticos de bitrate para streaming ou gravação.',
8
+ ui: {
9
+ presetLabel: 'Comece com uma cena', presetFast: 'Stream web rápido', presetUpload: 'Live diário', presetArchive: 'Arquivo 4K', resolutionLabel: 'Resolução', frameRateLabel: 'Fotogramas por segundo', codecLabel: 'Codec', bitrateLabel: 'Bitrate de vídeo', durationLabel: 'Duração da sessão', copiesLabel: 'Cópias guardadas', minutesLabel: 'minutos', copiesShort: 'cópias', h264: 'H.264', h265: 'H.265', av1: 'AV1', codecNote: 'A eficiência do codec muda a leitura de qualidade, não o cálculo do armazenamento.', sceneLabel: 'Do sinal ao armazenamento', signalSource: 'Imagem', codecGate: 'Codificação', storageReel: 'Armazenamento', qualityEstimate: 'Leitura de qualidade', storageEstimate: 'Armazenamento estimado', perCopy: 'Uma cópia', allCopies: 'Todas as cópias', perHour: 'Por hora', frameTime: 'Tempo por fotograma', dataPerFrame: 'Dados por fotograma', comparisonLabel: 'Comparação de armazenamento', lean: 'Leve', balanced: 'Equilibrado', crisp: 'Nítido', qualityLean: 'Leve e compacto', qualityBalanced: 'Sinal equilibrado', qualityStrong: 'Bom detalhe', qualityExcellent: 'Grande margem', qualityAggressive: 'Compressão agressiva', qualityGuidance: 'Uma estimativa visual para comparar definições.', capacityLight: 'Pegada de armazenamento leve', capacityMedium: 'Pegada de armazenamento média', capacityHeavy: 'Pegada de armazenamento elevada', capacityNote: 'O estado de capacidade usa o total de cópias mostrado acima.', reset: 'Repor valores', localNote: 'Funciona localmente neste navegador. Nada é enviado.', assumptionTitle: 'Ler os pressupostos', assumptionText: 'O armazenamento usa gigabytes decimais e o bitrate de vídeo indicado. Não inclui áudio, sobrecarga do contentor, picos de bitrate variável nem espaço do sistema de ficheiros.', warningText: 'Os níveis de qualidade são referências de planeamento. Movimento, grão, fotogramas-chave, predefinições do codificador, transcodificação e margem de rede podem alterar o resultado real.', readyText: 'Altere um valor para redesenhar o sinal.', calculateAria: 'Atualizar o plano de vídeo',
10
+ },
11
+ faq: [
12
+ { question: 'Este planeador envia ou inspeciona o meu vídeo?', answer: 'Não. Usa apenas os valores introduzidos no navegador. Não envia ficheiros, não inspeciona uma câmara nem consulta um serviço de streaming.' },
13
+ { question: 'Como é calculado o armazenamento?', answer: 'O bitrate é multiplicado pela duração e dividido por oito para converter bits em bytes. O resultado usa gigabytes decimais e é multiplicado pelo número de cópias.' },
14
+ { question: 'O que significa a leitura de qualidade?', answer: 'É uma regra indicativa baseada em píxeis, fotogramas por segundo, bitrate e um fator amplo de eficiência do codec. Não promete qualidade porque o movimento e o codificador também contam.' },
15
+ { question: 'Por que motivo o mesmo bitrate muda com outra resolução ou taxa de fotogramas?', answer: 'Uma resolução maior tem mais píxeis e uma taxa maior envia mais fotogramas por segundo. Mais informação visual tem de partilhar o mesmo bitrate.' },
16
+ { question: 'Posso usar o resultado como requisito de uma plataforma?', answer: 'Use-o para planear capacidade e comparar cenários. Os requisitos mudam, por isso consulte a orientação atual do destino e deixe margem de envio para uma transmissão ao vivo.' },
17
+ ],
18
+ howTo: [
19
+ { name: 'Escolha o formato da imagem', text: 'Selecione a resolução e a taxa de fotogramas que correspondem ao stream ou gravação que pretende criar.' },
20
+ { name: 'Defina o sinal', text: 'Escolha o codec e introduza o bitrate de vídeo em megabits por segundo. Um preset é um bom ponto de partida.' },
21
+ { name: 'Descreva a sessão', text: 'Indique a duração em minutos e o número de cópias que quer conservar, editar ou entregar.' },
22
+ { name: 'Leia a compensação', text: 'Compare os níveis leve, equilibrado e nítido para perceber a mudança de armazenamento antes da sessão.' },
23
+ ],
24
+ seo: [
25
+ { type: 'title', text: 'Estime o armazenamento de vídeo antes de transmitir ou gravar', level: 2 },
26
+ { type: 'paragraph', html: 'Uma calculadora de bitrate de vídeo ajuda a preparar um plano de armazenamento realista para uma sessão. Introduza bitrate, duração e cópias e compare três níveis de sinal para o mesmo formato de imagem.' },
27
+ { type: 'title', text: 'O que calcula o planeador', level: 3 },
28
+ { type: 'list', items: ['<strong>Armazenamento:</strong> bitrate multiplicado pelo tempo, convertido de bits para gigabytes decimais e multiplicado pelas cópias.', '<strong>Tempo por fotograma:</strong> milissegundos disponíveis por fotograma na FPS escolhida e uma estimativa de dados por fotograma.', '<strong>Leitura de qualidade:</strong> comparação de píxeis por fotograma ajustada por um fator de eficiência do codec.'] },
29
+ { type: 'title', text: 'Como a resolução e a FPS mudam a compensação', level: 3 },
30
+ { type: 'paragraph', html: 'A resolução aumenta os píxeis de cada fotograma e a FPS aumenta a quantidade de fotogramas por segundo. Se o bitrate ficar igual, cada fotograma recebe menos dados e a compressão fica mais exigente.' },
31
+ { type: 'tip', title: 'Deixe margem para uma transmissão ao vivo', html: 'Considere o bitrate de vídeo como a carga principal, não como toda a capacidade da ligação. Reserve espaço para áudio, protocolo e variações da rede e teste uma cena com movimento semelhante.' },
32
+ { type: 'title', text: 'Use as orientações da plataforma no ajuste final', level: 3 },
33
+ { type: 'paragraph', html: 'Este planeador é independente da plataforma. O YouTube publica intervalos de bitrate por resolução e taxa de fotogramas. Consulte as regras atuais do destino para validar o cenário.' },
34
+ { type: 'title', text: 'Por que motivo o resultado é uma estimativa', level: 3 },
35
+ { type: 'paragraph', html: 'Um bitrate nominal não descreve todos os bytes do ficheiro final. Bitrate variável, áudio, metadados do contentor, fotogramas-chave, transcodificação e unidades do sistema podem alterar o tamanho.' },
36
+ ],
37
+ });
@@ -0,0 +1,37 @@
1
+ import { createLocalizedContent } from '../locale-content';
2
+
3
+ export const content = createLocalizedContent({
4
+ language: 'ru',
5
+ slug: 'kalkulyator-bitreyta-hranilishcha-video',
6
+ title: 'Планировщик битрейта и хранилища видео',
7
+ description: 'Оцените объём видеохранилища, время кадра и практичные уровни битрейта для трансляции или записи.',
8
+ ui: {
9
+ presetLabel: 'Начать со сцены', presetFast: 'Быстрая веб-трансляция', presetUpload: 'Обычный эфир', presetArchive: 'Архив 4K', resolutionLabel: 'Разрешение', frameRateLabel: 'Частота кадров', codecLabel: 'Кодек', bitrateLabel: 'Битрейт видео', durationLabel: 'Длительность сеанса', copiesLabel: 'Сохраняемые копии', minutesLabel: 'минут', copiesShort: 'копий', h264: 'H.264', h265: 'H.265', av1: 'AV1', codecNote: 'Эффективность кодека меняет оценку качества, но не расчёт хранилища.', sceneLabel: 'От сигнала к хранилищу', signalSource: 'Изображение', codecGate: 'Кодирование', storageReel: 'Хранилище', qualityEstimate: 'Оценка качества', storageEstimate: 'Расчётный объём', perCopy: 'Одна копия', allCopies: 'Все копии', perHour: 'В час', frameTime: 'Время кадра', dataPerFrame: 'Данные на кадр', comparisonLabel: 'Сравнение объёма', lean: 'Экономный', balanced: 'Сбалансированный', crisp: 'Чёткий', qualityLean: 'Лёгкий сигнал', qualityBalanced: 'Сбалансированный сигнал', qualityStrong: 'Много деталей', qualityExcellent: 'Большой запас', qualityAggressive: 'Сильное сжатие', qualityGuidance: 'Визуальная оценка для сравнения настроек.', capacityLight: 'Небольшая нагрузка на хранилище', capacityMedium: 'Средняя нагрузка на хранилище', capacityHeavy: 'Большая нагрузка на хранилище', capacityNote: 'Статус ёмкости основан на общем числе копий выше.', reset: 'Сбросить значения', localNote: 'Работает локально в браузере. Ничего не загружается.', assumptionTitle: 'Допущения расчёта', assumptionText: 'Объём считается в десятичных гигабайтах по введённому битрейту видео. Аудио, служебные данные контейнера, пики переменного битрейта и место файловой системы не добавляются.', warningText: 'Уровни качества служат для планирования. Движение, зернистость, ключевые кадры, пресет кодировщика, перекодирование платформы и запас сети меняют реальный результат.', readyText: 'Измените значение, чтобы перерисовать сигнал.', calculateAria: 'Обновить план видео',
10
+ },
11
+ faq: [
12
+ { question: 'Загружает ли этот планировщик и проверяет ли он моё видео?', answer: 'Нет. Он использует только значения, введённые в браузере. Файлы не загружаются, камера не проверяется, запросы к сервисам трансляции не выполняются.' },
13
+ { question: 'Как рассчитывается объём хранилища?', answer: 'Битрейт умножается на длительность и делится на восемь для перевода битов в байты. Результат указан в десятичных гигабайтах и умножен на число копий.' },
14
+ { question: 'Что означает оценка качества?', answer: 'Это ориентир на основе пикселей, кадров в секунду, битрейта и общего коэффициента эффективности кодека. Это не гарантия качества, поскольку важны также движение и настройки кодировщика.' },
15
+ { question: 'Почему один и тот же битрейт меняется при другом разрешении или FPS?', answer: 'Большее разрешение содержит больше пикселей, а высокая частота отправляет больше кадров в секунду. Одному битрейту приходится описывать больше визуальной информации.' },
16
+ { question: 'Можно ли считать результат требованием платформы?', answer: 'Используйте его для планирования ёмкости и сравнения сценариев. Требования меняются, поэтому проверьте актуальную инструкцию площадки и оставьте запас исходящей скорости для эфира.' },
17
+ ],
18
+ howTo: [
19
+ { name: 'Выберите формат изображения', text: 'Укажите разрешение и частоту кадров для планируемой трансляции или записи.' },
20
+ { name: 'Настройте сигнал кодирования', text: 'Выберите кодек и введите битрейт видео в мегабитах в секунду. Для старта можно выбрать пресет.' },
21
+ { name: 'Опишите сеанс', text: 'Введите длительность в минутах и число копий, которые нужно сохранить, смонтировать или передать.' },
22
+ { name: 'Прочитайте компромисс', text: 'Сравните экономный, сбалансированный и чёткий уровни до начала записи.' },
23
+ ],
24
+ seo: [
25
+ { type: 'title', text: 'Оцените объём видео перед трансляцией или записью', level: 2 },
26
+ { type: 'paragraph', html: 'Калькулятор битрейта видео помогает составить реалистичный план хранилища для записи. Введите битрейт, длительность и число копий, а затем сравните три уровня сигнала для одного формата изображения.' },
27
+ { type: 'title', text: 'Что рассчитывает планировщик', level: 3 },
28
+ { type: 'list', items: ['<strong>Хранилище:</strong> битрейт умножается на время, переводится из битов в десятичные гигабайты и умножается на число копий.', '<strong>Время кадра:</strong> миллисекунды на кадр при выбранной частоте и оценка данных на кадр.', '<strong>Оценка качества:</strong> сравнение пикселей на кадр с поправкой на эффективность кодека.'] },
29
+ { type: 'title', text: 'Как разрешение и FPS меняют компромисс', level: 3 },
30
+ { type: 'paragraph', html: 'Разрешение увеличивает число пикселей в кадре, а FPS увеличивает число кадров в секунду. При неизменном битрейте каждому кадру достаётся меньше данных и сжатие становится сложнее.' },
31
+ { type: 'tip', title: 'Оставьте запас для эфира', html: 'Считайте битрейт видео основной нагрузкой, а не всей пропускной способностью. Оставьте место для звука, протокола и сетевых колебаний и протестируйте похожую по движению сцену.' },
32
+ { type: 'title', text: 'Проверьте правила платформы перед финальным выбором', level: 3 },
33
+ { type: 'paragraph', html: 'Планировщик не привязан к площадке. YouTube публикует диапазоны битрейта по разрешению и частоте кадров. Сверьте смоделированный сценарий с актуальными правилами назначения.' },
34
+ { type: 'title', text: 'Почему результат является оценкой', level: 3 },
35
+ { type: 'paragraph', html: 'Номинальный битрейт не описывает каждый байт готового файла. Переменный битрейт, звук, метаданные контейнера, ключевые кадры, перекодирование и единицы системы могут изменить размер.' },
36
+ ],
37
+ });
@@ -0,0 +1,37 @@
1
+ import { createLocalizedContent } from '../locale-content';
2
+
3
+ export const content = createLocalizedContent({
4
+ language: 'sv',
5
+ slug: 'video-bitrate-lagringsplanerare',
6
+ title: 'Planerare för videobitrate och lagring',
7
+ description: 'Uppskatta videolagring, bildtid och praktiska bitrate nivåer för streaming eller inspelning.',
8
+ ui: {
9
+ presetLabel: 'Börja med en scen', presetFast: 'Snabb webbstream', presetUpload: 'Vardaglig livesändning', presetArchive: '4K arkiv', resolutionLabel: 'Upplösning', frameRateLabel: 'Bildfrekvens', codecLabel: 'Kodek', bitrateLabel: 'Videobitrate', durationLabel: 'Sessionens längd', copiesLabel: 'Sparade kopior', minutesLabel: 'minuter', copiesShort: 'kopior', h264: 'H.264', h265: 'H.265', av1: 'AV1', codecNote: 'Kodekens effektivitet ändrar kvalitetsbedömningen, inte lagringsberäkningen.', sceneLabel: 'Signal till lagring', signalSource: 'Bild', codecGate: 'Kodning', storageReel: 'Lagring', qualityEstimate: 'Kvalitetsbedömning', storageEstimate: 'Uppskattad lagring', perCopy: 'En kopia', allCopies: 'Alla kopior', perHour: 'Per timme', frameTime: 'Bildtid', dataPerFrame: 'Data per bild', comparisonLabel: 'Jämförelse av lagring', lean: 'Snål', balanced: 'Balanserad', crisp: 'Skarp', qualityLean: 'Lätt och snål', qualityBalanced: 'Balanserad signal', qualityStrong: 'Starka detaljer', qualityExcellent: 'Gott om marginal', qualityAggressive: 'Kraftig komprimering', qualityGuidance: 'En visuell uppskattning för att jämföra inställningar.', capacityLight: 'Litet lagringsavtryck', capacityMedium: 'Medelstort lagringsavtryck', capacityHeavy: 'Stort lagringsavtryck', capacityNote: 'Kapacitetsstatusen baseras på det totala antalet kopior ovan.', reset: 'Återställ värden', localNote: 'Kör lokalt i webbläsaren. Ingenting laddas upp.', assumptionTitle: 'Läs antagandena', assumptionText: 'Lagringen använder decimala gigabyte och den angivna videobitraten. Ljud, containeröverhead, toppar i variabel bitrate och filsystemets utrymme läggs inte till.', warningText: 'Kvalitetsnivåerna är planeringsriktlinjer. Rörelse, korn, nyckelbilder, kodarpreset, plattformens omkodning och nätverksmarginal kan ändra det verkliga resultatet.', readyText: 'Ändra ett värde för att rita om signalen.', calculateAria: 'Uppdatera videoplanen',
10
+ },
11
+ faq: [
12
+ { question: 'Laddar eller granskar planeraren min video?', answer: 'Nej. Den använder bara värdena du anger i webbläsaren. Inga filer laddas upp, ingen kamera granskas och ingen streamingtjänst kontaktas.' },
13
+ { question: 'Hur beräknas lagringen?', answer: 'Bitrate multipliceras med tiden och delas med åtta för att omvandla bitar till byte. Resultatet använder decimala gigabyte och multipliceras med antalet kopior.' },
14
+ { question: 'Vad betyder kvalitetsbedömningen?', answer: 'Det är en tumregel baserad på pixlar, bilder per sekund, bitrate och en bred kodekeffektivitetsfaktor. Det är inget löfte om bildkvalitet eftersom rörelse och kodarinställningar också spelar roll.' },
15
+ { question: 'Varför ändras samma bitrate med annan upplösning eller bildfrekvens?', answer: 'En högre upplösning beskriver fler pixlar och en högre bildfrekvens skickar fler bilder per sekund. Mer visuell information måste dela samma bitrate.' },
16
+ { question: 'Kan resultatet användas som ett plattformskrav?', answer: 'Använd det för kapacitetsplanering och jämförelser. Plattformskrav ändras, så kontrollera den aktuella kodarguiden och lämna uppladdningsmarginal för en livesändning.' },
17
+ ],
18
+ howTo: [
19
+ { name: 'Välj bildformat', text: 'Välj upplösning och bildfrekvens som passar streamen eller inspelningen du planerar.' },
20
+ { name: 'Ställ in kodningssignalen', text: 'Välj kodek och ange videobitrate i megabit per sekund. Ett preset fungerar som startpunkt.' },
21
+ { name: 'Beskriv sessionen', text: 'Ange längden i minuter och hur många kopior du vill spara, redigera eller leverera.' },
22
+ { name: 'Läs avvägningen', text: 'Jämför snåla, balanserade och skarpa nivåer innan du börjar spela in.' },
23
+ ],
24
+ seo: [
25
+ { type: 'title', text: 'Uppskatta videolagring före streaming eller inspelning', level: 2 },
26
+ { type: 'paragraph', html: 'En videobitrate kalkylator hjälper när en inspelningssession behöver en realistisk lagringsplan. Ange bitrate, längd och kopior och jämför tre signalnivåer för samma bildformat.' },
27
+ { type: 'title', text: 'Det här beräknar planeraren', level: 3 },
28
+ { type: 'list', items: ['<strong>Lagring:</strong> bitrate gånger tid, omvandlad från bitar till decimala gigabyte och multiplicerad med kopiorna.', '<strong>Bildtid:</strong> millisekunder per bild vid vald FPS och en uppskattning av data per bild.', '<strong>Kvalitetsbedömning:</strong> en jämförelse av pixlar per bild med en kodekeffektivitetsfaktor.'] },
29
+ { type: 'title', text: 'Så påverkar upplösning och FPS avvägningen', level: 3 },
30
+ { type: 'paragraph', html: 'Upplösningen ökar antalet pixlar i varje bild och FPS ökar antalet bilder per sekund. Om bitraten är oförändrad får varje bild mindre data och komprimeringen blir mer krävande.' },
31
+ { type: 'tip', title: 'Lämna marginal för live', html: 'Se videobitraten som huvudlasten, inte som hela anslutningens kapacitet. Lämna plats för ljud, protokoll och nätverksvariationer och testa en scen med liknande rörelse.' },
32
+ { type: 'title', text: 'Använd plattformens riktlinjer för slutvärdet', level: 3 },
33
+ { type: 'paragraph', html: 'Planeraren är plattformsneutral. YouTube publicerar bitrateintervall efter upplösning och bildfrekvens. Kontrollera aktuella regler för destinationen innan du låser inställningen.' },
34
+ { type: 'title', text: 'Varför lagringsresultatet är en uppskattning', level: 3 },
35
+ { type: 'paragraph', html: 'En nominell bitrate beskriver inte varje byte i den färdiga filen. Variabel bitrate, ljud, containermetadata, nyckelbilder, omkodning, systemenheter och vald inspelningsprofil kan ändra slutstorleken.' },
36
+ ],
37
+ });
@@ -0,0 +1,37 @@
1
+ import { createLocalizedContent } from '../locale-content';
2
+
3
+ export const content = createLocalizedContent({
4
+ language: 'tr',
5
+ slug: 'video-bitrate-depolama-planlayici',
6
+ title: 'Video Bit Hızı ve Depolama Planlayıcısı',
7
+ description: 'Yayın veya kayıt için video depolamasını, kare süresini ve pratik bit hızı seviyelerini tahmin edin.',
8
+ ui: {
9
+ presetLabel: 'Bir sahneyle başlayın', presetFast: 'Hızlı web yayını', presetUpload: 'Günlük canlı yayın', presetArchive: '4K arşiv', resolutionLabel: 'Çözünürlük', frameRateLabel: 'Kare hızı', codecLabel: 'Kodek', bitrateLabel: 'Video bit hızı', durationLabel: 'Oturum süresi', copiesLabel: 'Saklanan kopyalar', minutesLabel: 'dakika', copiesShort: 'kopya', h264: 'H.264', h265: 'H.265', av1: 'AV1', codecNote: 'Kodek verimliliği kalite yorumunu değiştirir, depolama hesabını değiştirmez.', sceneLabel: 'Sinyalden depolamaya', signalSource: 'Görüntü', codecGate: 'Kodlama', storageReel: 'Depolama', qualityEstimate: 'Kalite yorumu', storageEstimate: 'Tahmini depolama', perCopy: 'Bir kopya', allCopies: 'Tüm kopyalar', perHour: 'Saat başına', frameTime: 'Kare süresi', dataPerFrame: 'Kare başına veri', comparisonLabel: 'Depolama karşılaştırması', lean: 'Hafif', balanced: 'Dengeli', crisp: 'Keskin', qualityLean: 'Hafif ve ekonomik', qualityBalanced: 'Dengeli sinyal', qualityStrong: 'Güçlü ayrıntı', qualityExcellent: 'Geniş pay', qualityAggressive: 'Agresif sıkıştırma', qualityGuidance: 'Ayarları karşılaştırmak için görsel bir tahmin.', capacityLight: 'Hafif depolama yükü', capacityMedium: 'Orta depolama yükü', capacityHeavy: 'Yüksek depolama yükü', capacityNote: 'Kapasite durumu yukarıdaki toplam kopyalara göre belirlenir.', reset: 'Değerleri sıfırla', localNote: 'Bu tarayıcıda yerel çalışır. Hiçbir şey yüklenmez.', assumptionTitle: 'Varsayımları okuyun', assumptionText: 'Depolama, girilen video bit hızını ve ondalık gigabaytları kullanır. Ses, kapsayıcı ek yükü, değişken bit hızı tepe değerleri ve dosya sistemi payı eklenmez.', warningText: 'Kalite seviyeleri planlama ölçütleridir. Hareket, gren, anahtar kareler, kodlayıcı ön ayarları, platform dönüştürmesi ve ağ payı gerçek sonucu değiştirebilir.', readyText: 'Sinyali yeniden çizmek için bir değeri değiştirin.', calculateAria: 'Video planını güncelle',
10
+ },
11
+ faq: [
12
+ { question: 'Bu planlayıcı videomu yükler veya inceler mi?', answer: 'Hayır. Yalnızca tarayıcıya girdiğiniz değerleri kullanır. Dosya yüklemez, kamerayı incelemez ve yayın hizmetine bağlanmaz.' },
13
+ { question: 'Depolama nasıl hesaplanır?', answer: 'Bit hızı süreyle çarpılır ve bitleri bayta çevirmek için sekize bölünür. Sonuç ondalık gigabayt olarak gösterilir ve kopya sayısıyla çarpılır.' },
14
+ { question: 'Kalite yorumu ne anlama gelir?', answer: 'Piksel, saniyedeki kare, bit hızı ve genel bir kodek verimlilik katsayısına dayanan pratik bir ölçüttür. Hareket ve kodlayıcı ayarları da önemli olduğu için görüntü kalitesi garantisi değildir.' },
15
+ { question: 'Aynı bit hızı farklı çözünürlük veya kare hızında neden değişir?', answer: 'Yüksek çözünürlükte daha fazla piksel, yüksek kare hızında saniyede daha fazla kare vardır. Daha fazla görsel bilgi aynı bit hızını paylaşır.' },
16
+ { question: 'Sonucu bir platform şartı olarak kullanabilir miyim?', answer: 'Kapasite planlamak ve senaryoları karşılaştırmak için kullanın. Platform şartları değişebilir; güncel kodlayıcı rehberini kontrol edin ve canlı yayın için yükleme payı bırakın.' },
17
+ ],
18
+ howTo: [
19
+ { name: 'Görüntü biçimini seçin', text: 'Planladığınız yayın veya kayda uygun çözünürlük ve kare hızını seçin.' },
20
+ { name: 'Kodlama sinyalini ayarlayın', text: 'Kodeği seçin ve video bit hızını saniyede megabit olarak girin. Başlangıç için bir ön ayar kullanabilirsiniz.' },
21
+ { name: 'Oturumu tanımlayın', text: 'Süreyi dakika olarak ve saklamak, düzenlemek veya teslim etmek istediğiniz kopya sayısını girin.' },
22
+ { name: 'Değiş tokuşu okuyun', text: 'Kayıttan önce hafif, dengeli ve keskin seviyelerin depolamayı nasıl değiştirdiğini karşılaştırın.' },
23
+ ],
24
+ seo: [
25
+ { type: 'title', text: 'Yayın veya kayıt öncesinde video depolamasını tahmin edin', level: 2 },
26
+ { type: 'paragraph', html: 'Video bit hızı hesaplayıcısı, bir kayıt oturumu için gerçekçi depolama planı gerektiğinde yardımcı olur. Bit hızını, süreyi ve kopyaları girin; aynı görüntü biçimi için üç sinyal seviyesini karşılaştırın.' },
27
+ { type: 'title', text: 'Planlayıcının hesapladıkları', level: 3 },
28
+ { type: 'list', items: ['<strong>Depolama:</strong> bit hızı zamanla çarpılır, bitten ondalık gigabayta çevrilir ve kopya sayısıyla çarpılır.', '<strong>Kare süresi:</strong> seçilen FPS değerinde her kare için kullanılabilen milisaniye ve kare başına veri tahmini.', '<strong>Kalite yorumu:</strong> kodek verimlilik katsayısıyla ayarlanmış kare başına piksel karşılaştırması.'] },
29
+ { type: 'title', text: 'Çözünürlük ve FPS değiş tokuşu nasıl etkiler', level: 3 },
30
+ { type: 'paragraph', html: 'Çözünürlük her karedeki piksel sayısını, FPS ise saniyedeki kare sayısını artırır. Bit hızı sabit kalırsa her kareye daha az veri düşer ve sıkıştırma zorlaşır.' },
31
+ { type: 'tip', title: 'Canlı yayın için pay bırakın', html: 'Video bit hızını bağlantının tamamı olarak görmeyin. Ses, protokol ve ağ değişimleri için alan bırakın ve gerçek yayına benzer hareket içeren bir sahneyi test edin.' },
32
+ { type: 'title', text: 'Son ayar için platform rehberini kullanın', level: 3 },
33
+ { type: 'paragraph', html: 'Bu planlayıcı platformdan bağımsızdır. YouTube çözünürlük ve kare hızına göre bit hızı aralıkları yayımlar. Burada oluşturduğunuz senaryoyu hedef platformun güncel kurallarıyla doğrulayın.' },
34
+ { type: 'title', text: 'Depolama sonucu neden tahmindir', level: 3 },
35
+ { type: 'paragraph', html: 'Nominal bit hızı son dosyadaki her baytı açıklamaz. Değişken bit hızı, ses, kapsayıcı meta verileri, anahtar kareler, dönüştürme ve sistem birimleri son boyutu değiştirebilir.' },
36
+ ],
37
+ });
@@ -0,0 +1,37 @@
1
+ import { createLocalizedContent } from '../locale-content';
2
+
3
+ export const content = createLocalizedContent({
4
+ language: 'zh',
5
+ slug: 'video-bitrate-storage-planner',
6
+ title: '视频码率与存储规划器',
7
+ description: '估算直播或录制所需的视频存储空间、帧时间和实用码率等级。',
8
+ ui: {
9
+ presetLabel: '从场景开始', presetFast: '快速网页直播', presetUpload: '日常直播', presetArchive: '4K 存档', resolutionLabel: '分辨率', frameRateLabel: '帧率', codecLabel: '编码器', bitrateLabel: '视频码率', durationLabel: '会话时长', copiesLabel: '保存副本', minutesLabel: '分钟', copiesShort: '副本', h264: 'H.264', h265: 'H.265', av1: 'AV1', codecNote: '编码器效率会改变质量判断,但不会改变存储空间的算术结果。', sceneLabel: '从信号到存储', signalSource: '画面', codecGate: '编码', storageReel: '存储', qualityEstimate: '质量判断', storageEstimate: '预计存储', perCopy: '单个副本', allCopies: '全部副本', perHour: '每小时', frameTime: '帧时间', dataPerFrame: '每帧数据', comparisonLabel: '存储对比', lean: '轻量', balanced: '均衡', crisp: '清晰', qualityLean: '轻量省空间', qualityBalanced: '均衡信号', qualityStrong: '细节充足', qualityExcellent: '余量充足', qualityAggressive: '强压缩', qualityGuidance: '用于比较设置的视觉估算。', capacityLight: '存储占用较小', capacityMedium: '中等存储占用', capacityHeavy: '存储占用较大', capacityNote: '容量状态根据上方显示的副本总数计算。', reset: '重置数值', localNote: '在此浏览器本地运行,不会上传内容。', assumptionTitle: '查看计算假设', assumptionText: '存储使用十进制 GB 和输入的视频码率计算。不包含音频、容器开销、可变码率峰值或文件系统空间。', warningText: '质量等级是规划参考。运动、颗粒、关键帧、编码器预设、平台转码和网络余量都会改变真实结果。', readyText: '修改数值即可重新绘制信号。', calculateAria: '更新视频计划',
10
+ },
11
+ faq: [
12
+ { question: '这个规划器会上传或检查我的视频吗?', answer: '不会。它只使用你在浏览器中输入的数值,不会上传文件、检查摄像头或查询直播服务。' },
13
+ { question: '存储空间如何计算?', answer: '将码率乘以时长,再除以 8 把比特换算为字节。结果使用十进制 GB,并乘以保存副本的数量。' },
14
+ { question: '质量判断是什么意思?', answer: '这是根据像素、每秒帧数、码率和宽泛的编码器效率系数得出的参考值。运动和编码器设置也会影响结果,因此它不是画质保证。' },
15
+ { question: '为什么分辨率或帧率变化后,相同码率的含义也会变化?', answer: '更高分辨率包含更多像素,更高帧率每秒发送更多画面。更多视觉信息需要共享同一个码率。' },
16
+ { question: '可以把结果当作平台要求吗?', answer: '请把它用于容量规划和方案比较。平台要求会变化,因此应查看目标平台最新的编码指南,并为直播保留上传余量。' },
17
+ ],
18
+ howTo: [
19
+ { name: '选择画面格式', text: '选择与你准备制作的直播或录制相符的分辨率和帧率。' },
20
+ { name: '设置编码信号', text: '选择编码器并输入 Mbps 格式的视频码率。不确定时可以先使用预设。' },
21
+ { name: '描述会话', text: '输入分钟数和需要保存、剪辑或交付的副本数量。' },
22
+ { name: '查看取舍', text: '比较轻量、均衡和清晰等级,提前了解录制前的存储变化。' },
23
+ ],
24
+ seo: [
25
+ { type: 'title', text: '在直播或录制前估算视频存储空间', level: 2 },
26
+ { type: 'paragraph', html: '视频码率计算器适合需要制定实际存储计划的录制会话。输入码率、时长和副本数量,然后比较同一画面格式下的三种信号等级。' },
27
+ { type: 'title', text: '规划器会计算什么', level: 3 },
28
+ { type: 'list', items: ['<strong>存储空间:</strong> 码率乘以时间,从比特换算为十进制 GB,再乘以副本数量。', '<strong>帧时间:</strong> 根据所选 FPS 计算每帧可用的毫秒数,并估算每帧数据。', '<strong>质量判断:</strong> 根据编码器效率系数调整后的每帧像素对比。'] },
29
+ { type: 'title', text: '分辨率和 FPS 如何改变取舍', level: 3 },
30
+ { type: 'paragraph', html: '分辨率会增加每帧的像素数量,FPS 会增加每秒的帧数。如果码率不变,每帧能分到的数据就会减少,压缩压力也会增加。' },
31
+ { type: 'tip', title: '为直播保留网络余量', html: '不要把视频码率视为网络连接的全部容量。应为音频、协议和网络波动留出空间,并使用与真实直播运动情况相近的场景进行测试。' },
32
+ { type: 'title', text: '最终设置请参考平台指南', level: 3 },
33
+ { type: 'paragraph', html: '本规划器不绑定特定平台。YouTube 会按照分辨率和帧率发布码率范围。请使用目标平台的最新规则验证这里建立的方案。' },
34
+ { type: 'title', text: '为什么存储结果只是估算', level: 3 },
35
+ { type: 'paragraph', html: '名义码率无法描述最终文件中的每个字节。可变码率、音频、容器元数据、关键帧、转码和系统单位都可能改变最终大小。正式制作前应保留实际测试文件,并用它核对容量。' },
36
+ ],
37
+ });
@@ -0,0 +1,13 @@
1
+ import { videoBitratePlanner } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+ import plannerCss from './video-bitrate-storage-planner.css?raw';
4
+
5
+ export * from './entry';
6
+
7
+ export const VIDEO_BITRATE_PLANNER_TOOL: ToolDefinition = {
8
+ entry: videoBitratePlanner,
9
+ Component: () => import('./component.astro'),
10
+ SEOComponent: () => import('./seo.astro'),
11
+ BibliographyComponent: () => import('./bibliography.astro'),
12
+ css: plannerCss,
13
+ };
@@ -0,0 +1,72 @@
1
+ import type { FAQPage, HowTo, SoftwareApplication, Thing, WithContext } from 'schema-dts';
2
+ import { bibliography } from './bibliography';
3
+ import type { ToolLocaleContent, FAQItem, HowToStep, SEOSection } from '../../types';
4
+ import type { VideoBitratePlannerUI } from './ui';
5
+
6
+ export interface VideoBitrateLocaleData {
7
+ slug: string;
8
+ title: string;
9
+ description: string;
10
+ ui: VideoBitratePlannerUI;
11
+ seo: SEOSection[];
12
+ faq: FAQItem[];
13
+ howTo: HowToStep[];
14
+ language: string;
15
+ }
16
+
17
+ function createFaqSchema(faq: FAQItem[]): WithContext<FAQPage> {
18
+ return {
19
+ '@context': 'https://schema.org',
20
+ '@type': 'FAQPage',
21
+ mainEntity: faq.map((item) => ({
22
+ '@type': 'Question',
23
+ name: item.question,
24
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
25
+ })),
26
+ };
27
+ }
28
+
29
+ function createHowToSchema(data: VideoBitrateLocaleData): WithContext<HowTo> {
30
+ return {
31
+ '@context': 'https://schema.org',
32
+ '@type': 'HowTo',
33
+ name: data.title,
34
+ description: data.description,
35
+ step: data.howTo.map((step, index) => ({
36
+ '@type': 'HowToStep',
37
+ position: index + 1,
38
+ name: step.name,
39
+ text: step.text,
40
+ })),
41
+ };
42
+ }
43
+
44
+ function createAppSchema(data: VideoBitrateLocaleData): WithContext<SoftwareApplication> {
45
+ return {
46
+ '@context': 'https://schema.org',
47
+ '@type': 'SoftwareApplication',
48
+ name: data.title,
49
+ description: data.description,
50
+ applicationCategory: 'UtilityApplication',
51
+ operatingSystem: 'All',
52
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
53
+ inLanguage: data.language,
54
+ };
55
+ }
56
+
57
+ export function createLocalizedContent(data: VideoBitrateLocaleData): ToolLocaleContent<VideoBitratePlannerUI> {
58
+ const faqSchema = createFaqSchema(data.faq);
59
+ const howToSchema = createHowToSchema(data);
60
+ const appSchema = createAppSchema(data);
61
+ return {
62
+ slug: data.slug,
63
+ title: data.title,
64
+ description: data.description,
65
+ ui: data.ui,
66
+ seo: data.seo,
67
+ faq: data.faq,
68
+ bibliography,
69
+ howTo: data.howTo,
70
+ schemas: [faqSchema as WithContext<Thing>, howToSchema as WithContext<Thing>, appSchema as WithContext<Thing>],
71
+ };
72
+ }
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { calculatePlan, RESOLUTION_PRESETS } from './logic';
3
+
4
+ const baseInput = {
5
+ resolutionId: '1080p',
6
+ fps: 60,
7
+ codec: 'h264' as const,
8
+ bitrateMbps: 8,
9
+ durationMinutes: 60,
10
+ copies: 1,
11
+ };
12
+
13
+ describe('video bitrate planner logic', () => {
14
+ it('contains the supported resolution presets', () => {
15
+ expect(RESOLUTION_PRESETS.map((item) => item.id)).toEqual(['720p', '1080p', '1440p', '2160p']);
16
+ });
17
+
18
+ it('converts bitrate and duration into storage', () => {
19
+ const result = calculatePlan(baseInput);
20
+ expect(result.perCopyGb).toBeCloseTo(3.6, 6);
21
+ expect(result.bitratePerHourGb).toBeCloseTo(3.6, 6);
22
+ expect(result.dataPerFrameKb).toBeCloseTo(16.6667, 3);
23
+ });
24
+
25
+ it('multiplies storage by the requested copies', () => {
26
+ const result = calculatePlan({ ...baseInput, copies: 3, durationMinutes: 90 });
27
+ expect(result.allCopiesGb).toBeCloseTo(16.2, 6);
28
+ expect(result.capacityTone).toBe('light');
29
+ });
30
+
31
+ it('recognizes codec efficiency in the quality estimate', () => {
32
+ const h264 = calculatePlan(baseInput);
33
+ const av1 = calculatePlan({ ...baseInput, codec: 'av1' });
34
+ expect(av1.quality.effectiveBpp).toBeGreaterThan(h264.quality.effectiveBpp);
35
+ });
36
+
37
+ it('clamps unsafe or incomplete values to useful defaults', () => {
38
+ const result = calculatePlan({
39
+ ...baseInput,
40
+ fps: Number.NaN,
41
+ bitrateMbps: -4,
42
+ durationMinutes: 0,
43
+ copies: 400,
44
+ });
45
+ expect(result.input.fps).toBe(60);
46
+ expect(result.input.bitrateMbps).toBe(8);
47
+ expect(result.input.durationMinutes).toBe(60);
48
+ expect(result.input.copies).toBe(99);
49
+ });
50
+ });
@@ -0,0 +1,158 @@
1
+ export type VideoCodec = 'h264' | 'h265' | 'av1';
2
+
3
+ export interface ResolutionPreset {
4
+ id: string;
5
+ label: string;
6
+ width: number;
7
+ height: number;
8
+ }
9
+
10
+ export interface PlannerInput {
11
+ resolutionId: string;
12
+ fps: number;
13
+ codec: VideoCodec;
14
+ bitrateMbps: number;
15
+ durationMinutes: number;
16
+ copies: number;
17
+ }
18
+
19
+ export interface QualityResult {
20
+ label: string;
21
+ tone: 'lean' | 'balanced' | 'strong' | 'excellent' | 'aggressive';
22
+ effectiveBpp: number;
23
+ guidance: string;
24
+ }
25
+
26
+ export interface ComparisonRow {
27
+ label: string;
28
+ bitrateMbps: number;
29
+ storageGb: number;
30
+ quality: string;
31
+ tone: QualityResult['tone'];
32
+ }
33
+
34
+ export interface PlannerResult {
35
+ input: PlannerInput;
36
+ resolution: ResolutionPreset;
37
+ bitratePerHourGb: number;
38
+ perCopyGb: number;
39
+ allCopiesGb: number;
40
+ frameTimeMs: number;
41
+ dataPerFrameKb: number;
42
+ quality: QualityResult;
43
+ capacityTone: 'light' | 'medium' | 'heavy';
44
+ comparison: ComparisonRow[];
45
+ }
46
+
47
+ export const RESOLUTION_PRESETS: ResolutionPreset[] = [
48
+ { id: '720p', label: '720p', width: 1280, height: 720 },
49
+ { id: '1080p', label: '1080p', width: 1920, height: 1080 },
50
+ { id: '1440p', label: '1440p', width: 2560, height: 1440 },
51
+ { id: '2160p', label: '4K', width: 3840, height: 2160 },
52
+ ];
53
+
54
+ const CODEC_EFFICIENCY: Record<VideoCodec, number> = {
55
+ h264: 1,
56
+ h265: 1.35,
57
+ av1: 1.55,
58
+ };
59
+
60
+ const QUALITY_GUIDANCE: Record<QualityResult['tone'], string> = {
61
+ aggressive: 'Expect visible compression in motion or detailed scenes.',
62
+ lean: 'A compact setting for simple scenes and limited upload capacity.',
63
+ balanced: 'A practical middle ground for everyday streaming and recording.',
64
+ strong: 'Healthy headroom for motion, text, and changing scenes.',
65
+ excellent: 'A generous setting for demanding motion and fine detail.',
66
+ };
67
+
68
+ function clamp(value: number, minimum: number, maximum: number): number {
69
+ return Math.min(Math.max(value, minimum), maximum);
70
+ }
71
+
72
+ function getResolution(id: string): ResolutionPreset {
73
+ return RESOLUTION_PRESETS.find((item) => item.id === id) ?? RESOLUTION_PRESETS[1]!;
74
+ }
75
+
76
+ function positive(value: number, fallback: number): number {
77
+ return Number.isFinite(value) && value > 0 ? value : fallback;
78
+ }
79
+
80
+ function qualityFromBpp(effectiveBpp: number): QualityResult {
81
+ const tone = getQualityTone(effectiveBpp);
82
+ return {
83
+ label: tone,
84
+ tone,
85
+ effectiveBpp,
86
+ guidance: QUALITY_GUIDANCE[tone],
87
+ };
88
+ }
89
+
90
+ function getQualityTone(effectiveBpp: number): QualityResult['tone'] {
91
+ if (effectiveBpp >= 0.16) return 'excellent';
92
+ if (effectiveBpp >= 0.1) return 'strong';
93
+ if (effectiveBpp >= 0.06) return 'balanced';
94
+ if (effectiveBpp >= 0.03) return 'lean';
95
+ return 'aggressive';
96
+ }
97
+
98
+ function estimateBitrate(input: PlannerInput, multiplier: number): number {
99
+ const resolution = getResolution(input.resolutionId);
100
+ const base = resolution.width * resolution.height * input.fps * 0.000000061;
101
+ return Math.max(0.5, base * multiplier / CODEC_EFFICIENCY[input.codec]);
102
+ }
103
+
104
+ function makeComparison(input: PlannerInput): ComparisonRow[] {
105
+ const tiers = [
106
+ { label: 'lean', multiplier: 0.65 },
107
+ { label: 'balanced', multiplier: 1 },
108
+ { label: 'crisp', multiplier: 1.45 },
109
+ ];
110
+ return tiers.map((tier) => {
111
+ const bitrateMbps = estimateBitrate(input, tier.multiplier);
112
+ const effectiveBpp = bitrateMbps * 1000000 * CODEC_EFFICIENCY[input.codec]
113
+ / (getResolution(input.resolutionId).width * getResolution(input.resolutionId).height * input.fps);
114
+ const quality = qualityFromBpp(effectiveBpp);
115
+ return {
116
+ label: tier.label,
117
+ bitrateMbps,
118
+ storageGb: bitrateMbps * input.durationMinutes * 60 / 8 / 1000,
119
+ quality: quality.label,
120
+ tone: quality.tone,
121
+ };
122
+ });
123
+ }
124
+
125
+ function capacityFromStorage(storageGb: number): PlannerResult['capacityTone'] {
126
+ if (storageGb >= 100) return 'heavy';
127
+ if (storageGb >= 20) return 'medium';
128
+ return 'light';
129
+ }
130
+
131
+ export function calculatePlan(rawInput: PlannerInput): PlannerResult {
132
+ const input: PlannerInput = {
133
+ resolutionId: rawInput.resolutionId,
134
+ fps: clamp(positive(rawInput.fps, 60), 1, 240),
135
+ codec: rawInput.codec,
136
+ bitrateMbps: clamp(positive(rawInput.bitrateMbps, 8), 0.1, 500),
137
+ durationMinutes: clamp(positive(rawInput.durationMinutes, 60), 1, 10080),
138
+ copies: Math.round(clamp(positive(rawInput.copies, 1), 1, 99)),
139
+ };
140
+ const resolution = getResolution(input.resolutionId);
141
+ const bitratePerHourGb = input.bitrateMbps * 60 * 60 / 8 / 1000;
142
+ const perCopyGb = input.bitrateMbps * input.durationMinutes / 8 / 1000 * 60;
143
+ const effectiveBpp = input.bitrateMbps * 1000000 * CODEC_EFFICIENCY[input.codec]
144
+ / (resolution.width * resolution.height * input.fps);
145
+ const quality = qualityFromBpp(effectiveBpp);
146
+ return {
147
+ input,
148
+ resolution,
149
+ bitratePerHourGb,
150
+ perCopyGb,
151
+ allCopiesGb: perCopyGb * input.copies,
152
+ frameTimeMs: 1000 / input.fps,
153
+ dataPerFrameKb: input.bitrateMbps * 1000 / input.fps / 8,
154
+ quality,
155
+ capacityTone: capacityFromStorage(perCopyGb * input.copies),
156
+ comparison: makeComparison(input),
157
+ };
158
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { VIDEO_BITRATE_PLANNER_TOOL } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await VIDEO_BITRATE_PLANNER_TOOL.entry.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,31 @@
1
+ import type { PlannerInput } from './logic';
2
+
3
+ const STORAGE_KEY = 'jjlmoya-video-bitrate-planner';
4
+
5
+ function isStoredInput(value: unknown): value is Partial<PlannerInput> {
6
+ return typeof value === 'object' && value !== null;
7
+ }
8
+
9
+ export function loadPlannerInput(fallback: PlannerInput): PlannerInput {
10
+ try {
11
+ const raw = window.localStorage.getItem(STORAGE_KEY);
12
+ if (!raw) return fallback;
13
+ const parsed: unknown = JSON.parse(raw);
14
+ if (!isStoredInput(parsed)) return fallback;
15
+ return { ...fallback, ...parsed };
16
+ } catch {
17
+ return fallback;
18
+ }
19
+ }
20
+
21
+ export function savePlannerInput(input: PlannerInput): void {
22
+ try {
23
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(input));
24
+ } catch {}
25
+ }
26
+
27
+ export function clearPlannerInput(): void {
28
+ try {
29
+ window.localStorage.removeItem(STORAGE_KEY);
30
+ } catch {}
31
+ }
@@ -0,0 +1,50 @@
1
+ export interface VideoBitratePlannerUI extends Record<string, string | string[]> {
2
+ presetLabel: string;
3
+ presetFast: string;
4
+ presetUpload: string;
5
+ presetArchive: string;
6
+ resolutionLabel: string;
7
+ frameRateLabel: string;
8
+ codecLabel: string;
9
+ bitrateLabel: string;
10
+ durationLabel: string;
11
+ copiesLabel: string;
12
+ minutesLabel: string;
13
+ copiesShort: string;
14
+ h264: string;
15
+ h265: string;
16
+ av1: string;
17
+ codecNote: string;
18
+ sceneLabel: string;
19
+ signalSource: string;
20
+ codecGate: string;
21
+ storageReel: string;
22
+ qualityEstimate: string;
23
+ storageEstimate: string;
24
+ perCopy: string;
25
+ allCopies: string;
26
+ perHour: string;
27
+ frameTime: string;
28
+ dataPerFrame: string;
29
+ comparisonLabel: string;
30
+ lean: string;
31
+ balanced: string;
32
+ crisp: string;
33
+ qualityLean: string;
34
+ qualityBalanced: string;
35
+ qualityStrong: string;
36
+ qualityExcellent: string;
37
+ qualityAggressive: string;
38
+ qualityGuidance: string;
39
+ capacityLight: string;
40
+ capacityMedium: string;
41
+ capacityHeavy: string;
42
+ capacityNote: string;
43
+ reset: string;
44
+ localNote: string;
45
+ assumptionTitle: string;
46
+ assumptionText: string;
47
+ warningText: string;
48
+ readyText: string;
49
+ calculateAria: string;
50
+ }