bare-media 1.2.1 → 1.4.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.
package/worker/media.js CHANGED
@@ -3,37 +3,118 @@ import fs from 'bare-fs'
3
3
  import fetch from 'bare-fetch'
4
4
  import getMimeType from 'get-mime-type'
5
5
 
6
- import { importCodec } from '../shared/codecs.js'
6
+ import { importCodec, supportsQuality } from '../shared/codecs.js'
7
7
  import { calculateFitDimensions } from './util'
8
8
 
9
9
  const DEFAULT_PREVIEW_FORMAT = 'image/webp'
10
10
 
11
11
  const animatableMimetypes = ['image/webp']
12
12
 
13
- export async function createPreview ({ path, mimetype, maxWidth, maxHeight, format, encoding }) {
13
+ export async function createPreview({
14
+ path,
15
+ mimetype,
16
+ maxWidth,
17
+ maxHeight,
18
+ maxFrames,
19
+ maxBytes,
20
+ format,
21
+ encoding
22
+ }) {
14
23
  mimetype = mimetype || getMimeType(path)
15
24
  format = format || DEFAULT_PREVIEW_FORMAT
16
25
 
17
26
  const buffer = fs.readFileSync(path)
18
- const rgba = await decodeImageToRGBA(buffer, mimetype)
27
+ const rgba = await decodeImageToRGBA(buffer, mimetype, maxFrames)
19
28
  const { width, height } = rgba
20
29
 
21
- const { dimensions, rgba: maybeResizedRGBA } = await resizeRGBA(rgba, width, height, maxWidth, maxHeight)
30
+ const maybeResizedRGBA = await resizeRGBA(rgba, maxWidth, maxHeight)
31
+
32
+ let preview = await encodeImageFromRGBA(maybeResizedRGBA, format)
33
+
34
+ // quality reduction
35
+
36
+ if (maxBytes && preview.byteLength > maxBytes && supportsQuality(format)) {
37
+ const MIN_QUALITY = 50
38
+ for (let quality = 80; quality >= MIN_QUALITY; quality -= 15) {
39
+ preview = await encodeImageFromRGBA(maybeResizedRGBA, format, { quality })
40
+ if (preview.byteLength <= maxBytes) {
41
+ break
42
+ }
43
+ }
44
+ }
45
+
46
+ // fps reduction
47
+
48
+ if (
49
+ maxBytes &&
50
+ preview.byteLength > maxBytes &&
51
+ maybeResizedRGBA.frames?.length > 1
52
+ ) {
53
+ const quality = 75
54
+
55
+ // drop every n frame
56
+
57
+ for (const dropEvery of [4, 3, 2]) {
58
+ const frames = maybeResizedRGBA.frames.filter(
59
+ (frame, index) => index % dropEvery !== 0
60
+ )
61
+ const filtered = { ...maybeResizedRGBA, frames }
62
+ preview = await encodeImageFromRGBA(filtered, format, { quality })
63
+ if (!maxBytes || preview.byteLength <= maxBytes) {
64
+ break
65
+ }
66
+ }
67
+
68
+ // cap to 25 frames
69
+
70
+ if (preview.byteLength > maxBytes) {
71
+ const frames = maybeResizedRGBA.frames
72
+ .slice(0, 50)
73
+ .filter((frame, index) => index % 2 === 0)
74
+ const capped = { ...maybeResizedRGBA, frames }
75
+ preview = await encodeImageFromRGBA(capped, format, { quality })
76
+ }
77
+
78
+ // take only one frame
22
79
 
23
- const encoded = await encodeImageFromRGBA(maybeResizedRGBA, format, encoding)
80
+ if (preview.byteLength > maxBytes) {
81
+ const oneFrame = {
82
+ ...maybeResizedRGBA,
83
+ frames: maybeResizedRGBA.frames.slice(0, 1)
84
+ }
85
+ preview = await encodeImageFromRGBA(oneFrame, format)
86
+ }
87
+ }
88
+
89
+ if (maxBytes && preview.byteLength > maxBytes) {
90
+ throw new Error(
91
+ `Could not create preview under maxBytes, reached ${preview.byteLength} bytes`
92
+ )
93
+ }
94
+
95
+ const encoded =
96
+ encoding === 'base64'
97
+ ? { inlined: b4a.toString(preview, 'base64') }
98
+ : { buffer: preview }
24
99
 
25
100
  return {
26
101
  metadata: {
27
102
  dimensions: { width, height }
28
103
  },
29
104
  preview: {
30
- metadata: { mimetype: format, dimensions },
105
+ metadata: {
106
+ mimetype: format,
107
+ dimensions: {
108
+ width: maybeResizedRGBA.width,
109
+ height: maybeResizedRGBA.height
110
+ }
111
+ },
31
112
  ...encoded
32
113
  }
33
114
  }
34
115
  }
35
116
 
36
- export async function decodeImage ({ path, httpLink, mimetype }) {
117
+ export async function decodeImage({ path, httpLink, mimetype }) {
37
118
  let buffer
38
119
 
39
120
  if (path) {
@@ -54,15 +135,19 @@ export async function decodeImage ({ path, httpLink, mimetype }) {
54
135
  }
55
136
  }
56
137
 
57
- async function decodeImageToRGBA (buffer, mimetype) {
138
+ async function decodeImageToRGBA(buffer, mimetype, maxFrames) {
58
139
  let rgba
59
140
 
60
141
  const codec = await importCodec(mimetype)
61
142
 
62
143
  if (animatableMimetypes.includes(mimetype)) {
63
- const { frames, width, height } = codec.decodeAnimated(buffer)
64
- const { data } = frames.next().value
65
- rgba = { width, height, data }
144
+ const { width, height, loops, frames } = codec.decodeAnimated(buffer)
145
+ const data = []
146
+ for (const frame of frames) {
147
+ if (maxFrames > 0 && data.length >= maxFrames) break
148
+ data.push(frame)
149
+ }
150
+ rgba = { width, height, loops, frames: data }
66
151
  } else {
67
152
  rgba = codec.decode(buffer)
68
153
  }
@@ -70,26 +155,50 @@ async function decodeImageToRGBA (buffer, mimetype) {
70
155
  return rgba
71
156
  }
72
157
 
73
- async function encodeImageFromRGBA (rgba, format, encoding) {
158
+ async function encodeImageFromRGBA(rgba, format, opts) {
74
159
  const codec = await importCodec(format)
75
- const encoded = codec.encode(rgba)
76
160
 
77
- return encoding === 'base64'
78
- ? { inlined: b4a.toString(encoded, 'base64') }
79
- : { buffer: encoded }
161
+ let encoded
162
+ if (Array.isArray(rgba.frames)) {
163
+ encoded = codec.encodeAnimated(rgba, opts)
164
+ } else {
165
+ encoded = codec.encode(rgba, opts)
166
+ }
167
+
168
+ return encoded
80
169
  }
81
170
 
82
- async function resizeRGBA (rgba, width, height, maxWidth, maxHeight) {
83
- let maybeResizedRGBA, dimensions
171
+ async function resizeRGBA(rgba, maxWidth, maxHeight) {
172
+ const { width, height } = rgba
173
+
174
+ let maybeResizedRGBA
84
175
 
85
176
  if (maxWidth && maxHeight && (width > maxWidth || height > maxHeight)) {
86
177
  const { resize } = await import('bare-image-resample')
87
- dimensions = calculateFitDimensions(width, height, maxWidth, maxHeight)
88
- maybeResizedRGBA = resize(rgba, dimensions.width, dimensions.height)
178
+ const dimensions = calculateFitDimensions(
179
+ width,
180
+ height,
181
+ maxWidth,
182
+ maxHeight
183
+ )
184
+ if (Array.isArray(rgba.frames)) {
185
+ const frames = []
186
+ for (const frame of rgba.frames) {
187
+ const resized = resize(frame, dimensions.width, dimensions.height)
188
+ frames.push({ ...resized, timestamp: frame.timestamp })
189
+ }
190
+ maybeResizedRGBA = {
191
+ width: frames[0].width,
192
+ height: frames[0].height,
193
+ loops: rgba.loops,
194
+ frames
195
+ }
196
+ } else {
197
+ maybeResizedRGBA = resize(rgba, dimensions.width, dimensions.height)
198
+ }
89
199
  } else {
90
- dimensions = { width, height }
91
200
  maybeResizedRGBA = rgba
92
201
  }
93
202
 
94
- return { dimensions, rgba: maybeResizedRGBA }
203
+ return maybeResizedRGBA
95
204
  }
package/worker/util.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const log = (...args) => console.log('[bare-media]', ...args)
2
2
 
3
- export function calculateFitDimensions (width, height, maxWidth, maxHeight) {
3
+ export function calculateFitDimensions(width, height, maxWidth, maxHeight) {
4
4
  if (width <= maxWidth && height <= maxHeight) {
5
5
  return { width, height }
6
6
  }