bare-media 1.3.0 → 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/README.md CHANGED
@@ -49,13 +49,33 @@ const data = await createPreview({ path, maxWidth, maxHeight })
49
49
 
50
50
  ## API
51
51
 
52
- | Method | Parameters | Return Value | Description |
53
- | --------------- | ------------------------------------------------------- | ------------------- | ---------------------------------- |
54
- | `createPreview` | `path, mimetype, maxWidth, maxHeight, format, encoding` | `metadata, preview` | Create a preview from a media file |
55
- | `decodeImage` | `path`, `httpLink, mimetype` | `metadata, data` | Decode an image to RGBA |
56
-
57
52
  > See [schema.js](shared/spec/schema.js) for the complete reference of parameters
58
53
 
54
+ ### createPreview()
55
+
56
+ Create a preview from a media file
57
+
58
+ | Property | Type | Description |
59
+ | ----------- | ------ | ----------------------------------------------------------------- |
60
+ | `path` | string | Path to the input file |
61
+ | `mimetype` | string | Media type of the input file. If not provided it will be detected |
62
+ | `maxWidth` | number | Max width for the generated preview |
63
+ | `maxHeight` | number | Max height for the generated preview |
64
+ | `maxFrames` | number | Max frames for the generated preview in case the file is animated |
65
+ | `maxBytes` | number | Max bytes for the generated preview |
66
+ | `format` | string | Media type for the generated preview. Default `image/webp` |
67
+ | `encoding` | string | `base64` or nothing for buffer |
68
+
69
+ ### decodeImage()
70
+
71
+ Decode an image to RGBA
72
+
73
+ | Property | Type | Description |
74
+ | ---------- | ------ | ----------------------------------------------------------------- |
75
+ | `path` | string | Path to the input file. Either this or `httpLink` is required |
76
+ | `httpLink` | string | Http link to the input file |
77
+ | `mimetype` | string | Media type of the input file. If not provided it will be detected |
78
+
59
79
  ## License
60
80
 
61
81
  Apache-2.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bare-media",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
package/shared/codecs.js CHANGED
@@ -19,3 +19,7 @@ export async function importCodec(mimetype) {
19
19
  if (!codecImport) throw new Error(`No codec for ${mimetype}`)
20
20
  return await codecImport()
21
21
  }
22
+
23
+ export function supportsQuality(mimetype) {
24
+ return { 'image/webp': true, 'image/jpeg': true }[mimetype] || false
25
+ }
@@ -1,5 +1,6 @@
1
1
  // This file is autogenerated by the hrpc compiler
2
2
  /* eslint-disable camelcase */
3
+ /* eslint-disable space-before-function-paren */
3
4
 
4
5
  import { c, RPC, RPCStream, RPCRequestStream } from 'hrpc/runtime'
5
6
  import { getEncoding } from './messages.js'
@@ -140,8 +141,11 @@ class HRPC {
140
141
  return [].includes(command)
141
142
  }
142
143
 
144
+ // prettier-ignore-start
143
145
  _requestIsSend(command) {
144
- return [].includes(command)
146
+ return [
147
+ // prettier-ignore
148
+ ].includes(command)
145
149
  }
146
150
  }
147
151
 
@@ -101,11 +101,13 @@ const encoding2 = {
101
101
  const encoding3 = {
102
102
  preencode(state, m) {
103
103
  c.string.preencode(state, m.path)
104
- state.end++ // max flag is 16 so always one byte
104
+ state.end++ // max flag is 64 so always one byte
105
105
 
106
106
  if (m.mimetype) c.string.preencode(state, m.mimetype)
107
107
  if (m.maxWidth) c.uint.preencode(state, m.maxWidth)
108
108
  if (m.maxHeight) c.uint.preencode(state, m.maxHeight)
109
+ if (m.maxFrames) c.uint.preencode(state, m.maxFrames)
110
+ if (m.maxBytes) c.uint.preencode(state, m.maxBytes)
109
111
  if (m.format) c.string.preencode(state, m.format)
110
112
  if (m.encoding) c.string.preencode(state, m.encoding)
111
113
  },
@@ -114,8 +116,10 @@ const encoding3 = {
114
116
  (m.mimetype ? 1 : 0) |
115
117
  (m.maxWidth ? 2 : 0) |
116
118
  (m.maxHeight ? 4 : 0) |
117
- (m.format ? 8 : 0) |
118
- (m.encoding ? 16 : 0)
119
+ (m.maxFrames ? 8 : 0) |
120
+ (m.maxBytes ? 16 : 0) |
121
+ (m.format ? 32 : 0) |
122
+ (m.encoding ? 64 : 0)
119
123
 
120
124
  c.string.encode(state, m.path)
121
125
  c.uint.encode(state, flags)
@@ -123,6 +127,8 @@ const encoding3 = {
123
127
  if (m.mimetype) c.string.encode(state, m.mimetype)
124
128
  if (m.maxWidth) c.uint.encode(state, m.maxWidth)
125
129
  if (m.maxHeight) c.uint.encode(state, m.maxHeight)
130
+ if (m.maxFrames) c.uint.encode(state, m.maxFrames)
131
+ if (m.maxBytes) c.uint.encode(state, m.maxBytes)
126
132
  if (m.format) c.string.encode(state, m.format)
127
133
  if (m.encoding) c.string.encode(state, m.encoding)
128
134
  },
@@ -135,8 +141,10 @@ const encoding3 = {
135
141
  mimetype: (flags & 1) !== 0 ? c.string.decode(state) : null,
136
142
  maxWidth: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
137
143
  maxHeight: (flags & 4) !== 0 ? c.uint.decode(state) : 0,
138
- format: (flags & 8) !== 0 ? c.string.decode(state) : null,
139
- encoding: (flags & 16) !== 0 ? c.string.decode(state) : null
144
+ maxFrames: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
145
+ maxBytes: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
146
+ format: (flags & 32) !== 0 ? c.string.decode(state) : null,
147
+ encoding: (flags & 64) !== 0 ? c.string.decode(state) : null
140
148
  }
141
149
  }
142
150
  }
@@ -101,11 +101,13 @@ const encoding2 = {
101
101
  const encoding3 = {
102
102
  preencode(state, m) {
103
103
  c.string.preencode(state, m.path)
104
- state.end++ // max flag is 16 so always one byte
104
+ state.end++ // max flag is 64 so always one byte
105
105
 
106
106
  if (m.mimetype) c.string.preencode(state, m.mimetype)
107
107
  if (m.maxWidth) c.uint.preencode(state, m.maxWidth)
108
108
  if (m.maxHeight) c.uint.preencode(state, m.maxHeight)
109
+ if (m.maxFrames) c.uint.preencode(state, m.maxFrames)
110
+ if (m.maxBytes) c.uint.preencode(state, m.maxBytes)
109
111
  if (m.format) c.string.preencode(state, m.format)
110
112
  if (m.encoding) c.string.preencode(state, m.encoding)
111
113
  },
@@ -114,8 +116,10 @@ const encoding3 = {
114
116
  (m.mimetype ? 1 : 0) |
115
117
  (m.maxWidth ? 2 : 0) |
116
118
  (m.maxHeight ? 4 : 0) |
117
- (m.format ? 8 : 0) |
118
- (m.encoding ? 16 : 0)
119
+ (m.maxFrames ? 8 : 0) |
120
+ (m.maxBytes ? 16 : 0) |
121
+ (m.format ? 32 : 0) |
122
+ (m.encoding ? 64 : 0)
119
123
 
120
124
  c.string.encode(state, m.path)
121
125
  c.uint.encode(state, flags)
@@ -123,6 +127,8 @@ const encoding3 = {
123
127
  if (m.mimetype) c.string.encode(state, m.mimetype)
124
128
  if (m.maxWidth) c.uint.encode(state, m.maxWidth)
125
129
  if (m.maxHeight) c.uint.encode(state, m.maxHeight)
130
+ if (m.maxFrames) c.uint.encode(state, m.maxFrames)
131
+ if (m.maxBytes) c.uint.encode(state, m.maxBytes)
126
132
  if (m.format) c.string.encode(state, m.format)
127
133
  if (m.encoding) c.string.encode(state, m.encoding)
128
134
  },
@@ -135,8 +141,10 @@ const encoding3 = {
135
141
  mimetype: (flags & 1) !== 0 ? c.string.decode(state) : null,
136
142
  maxWidth: (flags & 2) !== 0 ? c.uint.decode(state) : 0,
137
143
  maxHeight: (flags & 4) !== 0 ? c.uint.decode(state) : 0,
138
- format: (flags & 8) !== 0 ? c.string.decode(state) : null,
139
- encoding: (flags & 16) !== 0 ? c.string.decode(state) : null
144
+ maxFrames: (flags & 8) !== 0 ? c.uint.decode(state) : 0,
145
+ maxBytes: (flags & 16) !== 0 ? c.uint.decode(state) : 0,
146
+ format: (flags & 32) !== 0 ? c.string.decode(state) : null,
147
+ encoding: (flags & 64) !== 0 ? c.string.decode(state) : null
140
148
  }
141
149
  }
142
150
  }
@@ -94,6 +94,16 @@
94
94
  "type": "uint",
95
95
  "version": 1
96
96
  },
97
+ {
98
+ "name": "maxFrames",
99
+ "type": "uint",
100
+ "version": 1
101
+ },
102
+ {
103
+ "name": "maxBytes",
104
+ "type": "uint",
105
+ "version": 1
106
+ },
97
107
  {
98
108
  "name": "format",
99
109
  "type": "string",
@@ -77,6 +77,14 @@ media.register({
77
77
  name: 'maxHeight',
78
78
  type: 'uint'
79
79
  },
80
+ {
81
+ name: 'maxFrames',
82
+ type: 'uint'
83
+ },
84
+ {
85
+ name: 'maxBytes',
86
+ type: 'uint'
87
+ },
80
88
  {
81
89
  name: 'format',
82
90
  type: 'string'
package/worker/media.js CHANGED
@@ -3,7 +3,7 @@ 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'
@@ -15,6 +15,8 @@ export async function createPreview({
15
15
  mimetype,
16
16
  maxWidth,
17
17
  maxHeight,
18
+ maxFrames,
19
+ maxBytes,
18
20
  format,
19
21
  encoding
20
22
  }) {
@@ -22,12 +24,78 @@ export async function createPreview({
22
24
  format = format || DEFAULT_PREVIEW_FORMAT
23
25
 
24
26
  const buffer = fs.readFileSync(path)
25
- const rgba = await decodeImageToRGBA(buffer, mimetype)
27
+ const rgba = await decodeImageToRGBA(buffer, mimetype, maxFrames)
26
28
  const { width, height } = rgba
27
29
 
28
30
  const maybeResizedRGBA = await resizeRGBA(rgba, maxWidth, maxHeight)
29
31
 
30
- const encoded = await encodeImageFromRGBA(maybeResizedRGBA, format, encoding)
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
79
+
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 }
31
99
 
32
100
  return {
33
101
  metadata: {
@@ -67,7 +135,7 @@ export async function decodeImage({ path, httpLink, mimetype }) {
67
135
  }
68
136
  }
69
137
 
70
- async function decodeImageToRGBA(buffer, mimetype) {
138
+ async function decodeImageToRGBA(buffer, mimetype, maxFrames) {
71
139
  let rgba
72
140
 
73
141
  const codec = await importCodec(mimetype)
@@ -76,6 +144,7 @@ async function decodeImageToRGBA(buffer, mimetype) {
76
144
  const { width, height, loops, frames } = codec.decodeAnimated(buffer)
77
145
  const data = []
78
146
  for (const frame of frames) {
147
+ if (maxFrames > 0 && data.length >= maxFrames) break
79
148
  data.push(frame)
80
149
  }
81
150
  rgba = { width, height, loops, frames: data }
@@ -86,19 +155,17 @@ async function decodeImageToRGBA(buffer, mimetype) {
86
155
  return rgba
87
156
  }
88
157
 
89
- async function encodeImageFromRGBA(rgba, format, encoding) {
158
+ async function encodeImageFromRGBA(rgba, format, opts) {
90
159
  const codec = await importCodec(format)
91
160
 
92
161
  let encoded
93
162
  if (Array.isArray(rgba.frames)) {
94
- encoded = codec.encodeAnimated(rgba)
163
+ encoded = codec.encodeAnimated(rgba, opts)
95
164
  } else {
96
- encoded = codec.encode(rgba)
165
+ encoded = codec.encode(rgba, opts)
97
166
  }
98
167
 
99
- return encoding === 'base64'
100
- ? { inlined: b4a.toString(encoded, 'base64') }
101
- : { buffer: encoded }
168
+ return encoded
102
169
  }
103
170
 
104
171
  async function resizeRGBA(rgba, maxWidth, maxHeight) {