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 +25 -5
- package/package.json +1 -1
- package/shared/codecs.js +4 -0
- package/shared/spec/hrpc/index.js +5 -1
- package/shared/spec/hrpc/messages.js +13 -5
- package/shared/spec/schema/index.js +13 -5
- package/shared/spec/schema/schema.json +10 -0
- package/shared/spec/schema.js +8 -0
- package/worker/media.js +77 -10
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
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 [
|
|
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
|
|
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.
|
|
118
|
-
(m.
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
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.
|
|
118
|
-
(m.
|
|
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
|
-
|
|
139
|
-
|
|
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
|
}
|
package/shared/spec/schema.js
CHANGED
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
|
-
|
|
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,
|
|
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
|
|
100
|
-
? { inlined: b4a.toString(encoded, 'base64') }
|
|
101
|
-
: { buffer: encoded }
|
|
168
|
+
return encoded
|
|
102
169
|
}
|
|
103
170
|
|
|
104
171
|
async function resizeRGBA(rgba, maxWidth, maxHeight) {
|