Repository navigation
Expand file tree
/
Copy pathNativeVideo.lua
More file actions
251 lines (213 loc) · 7.82 KB
/
Copy pathNativeVideo.lua
File metadata and controls
251 lines (213 loc) · 7.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
local ffi = require("ffi")
ffi.cdef[[
typedef struct nv_video nv_video;
typedef struct nv_info { int32_t width, height; double fps, duration; } nv_info;
nv_video* nv_open(const char* path_utf8);
void nv_get_info(nv_video* v, nv_info* out);
int nv_read_frame(nv_video* v, uint8_t* rgba_out, double* pts_out);
int nv_seek(nv_video* v, double seconds);
void nv_close(nv_video* v);
const char* nv_last_error(void);
const char* nv_backend(void);
]]
local NativeVideo = {}
NativeVideo.__index = NativeVideo
local SEEK_THRESHOLD = 1.0
local lib
local lf = love.filesystem
local newImage = love.graphics.newImage or love.graphics.newTexture
local function load_library()
if lib then return lib end
local os_name = love.system.getOS()
local file = os_name == "Windows" and "nativevideo.dll"
or os_name == "OS X" and "libnativevideo.dylib"
or "libnativevideo.so"
--for packing
local candidates = {}
if not lf.isFused() then
table.insert(candidates, lf.getSource() .. "/" .. file)
end
table.insert(candidates, lf.getSourceBaseDirectory() .. "/" .. file)
table.insert(candidates, "nativevideo")
local errors = {}
for _, path in ipairs(candidates) do
local ok, result = pcall(ffi.load, path)
if ok then lib = result; return lib end
table.insert(errors, result)
end
error("NativeVideo: could not load " .. file .. ":\n " .. table.concat(errors, "\n "), 3)
end
local function file_exists(path)
local f = io.open(path, "rb")
if f then f:close() return true end
return false
end
local function is_absolute(path)
return path:sub(1, 1) == "/" or path:match("^%a:[/\\]") ~= nil
end
local function resolve_path(path)
if is_absolute(path) and file_exists(path) then return path end
if lf.getInfo(path, "file") then
local dir = lf.getRealDirectory(path)
if dir then
local real = dir .. "/" .. path
if file_exists(real) then return real end
end
local cache = "nativevideo_cache/" .. path:gsub("[/\\]", "_")
if not lf.getInfo(cache, "file") then
lf.createDirectory("nativevideo_cache")
local data = assert(lf.read("data", path))
assert(lf.write(cache, data))
data:release()
end
return lf.getSaveDirectory() .. "/" .. cache
end
error("NativeVideo: file not found: " .. tostring(path), 3)
end
function NativeVideo.new(path)
local L = load_library()
local real_path = resolve_path(path)
local handle = L.nv_open(real_path)
if handle == nil then
error("NativeVideo: " .. ffi.string(L.nv_last_error()), 2)
end
handle = ffi.gc(handle, L.nv_close) -- closed automatically
local info = ffi.new("nv_info")
L.nv_get_info(handle, info)
local self = setmetatable({}, NativeVideo)
self.handle = handle
self.path = real_path
self.width = info.width
self.height = info.height
self.fps = info.fps > 0 and info.fps or 30
self.duration = info.duration
-- Two CPU-side frames: front is what's on screen back holds the next
self.front = love.image.newImageData(self.width, self.height, "rgba8")
self.back = love.image.newImageData(self.width, self.height, "rgba8")
self.image = love.graphics.newImage(self.front)
self.pts = ffi.new("double[1]")
self.time = 0 -- current playback position (seconds)
self.backPts = nil
self.eof = false
self.playing = false
self.loop = false
self.speed = 1
self.frameIndex = 0 -- frames shown so far (for stats)
self:_decodeIntoBack()
self:_presentBack()
return self
end
---Not gonna pretend to understand this thanks bestie <3
-- Decode the next frame into `back`. Returns true if a frame was read.
function NativeVideo:_decodeIntoBack()
local ptr = ffi.cast("uint8_t*", self.back:getFFIPointer())
local r = lib.nv_read_frame(self.handle, ptr, self.pts)
if r == 1 then
if not self.ptsOffset then self.ptsOffset = self.pts[0] end
self.backPts = self.pts[0] - self.ptsOffset
return true
elseif r == 0 then
self.backPts, self.eof = nil, true
if self.duration <= 0 then self.duration = self.time end
return false
else
error("NativeVideo: " .. ffi.string(lib.nv_last_error()), 3)
end
end
-- Swap the waiting frame onto the screen and start decoding the next one.
function NativeVideo:_presentBack()
self.front, self.back = self.back, self.front
self.dirty = true
self.frameIndex = self.frameIndex + 1
self:_decodeIntoBack()
end
-- Show every frame whose time has come
function NativeVideo:_advanceTo(t)
-- exact frame time, which would otherwise show the frame one tick late.
t = t + 1e-6
while self.backPts and self.backPts <= t do
self:_presentBack()
end
if self.dirty then
self.image:replacePixels(self.front)
self.dirty = false
end
end
function NativeVideo:update(dt)
if not self.playing then return end
self.time = self.time + dt * self.speed
self:_advanceTo(self.time)
if self.eof and self.duration > 0 and self.time >= self.duration then
if self.loop then
self:seek(self.time % self.duration)
else
self.playing = false
end
end
end
function NativeVideo:seek(t)
t = math.max(0, t)
if self.duration > 0 then t = math.min(t, self.duration) end
local target = t + 1e-6
local offset = self.ptsOffset or 0
-- starts at the target, Media Foundation may land on the keyframe before
local margin = 0
for _ = 1, 5 do
if lib.nv_seek(self.handle, math.max(0, t + offset - margin)) ~= 1 then
error("NativeVideo: " .. ffi.string(lib.nv_last_error()), 2)
end
self.eof = false
local got = self:_decodeIntoBack()
if not got or self.backPts <= target or t + offset - margin <= 0 then break end
margin = margin + (self.backPts - t) + 1 / self.fps
end
self.time = t
-- Show the last frame that starts at or before t.
if self.backPts then
self:_presentBack()
while self.backPts and self.backPts <= target do
self:_presentBack()
end
end
self.image:replacePixels(self.front)
self.dirty = false
end
--- Follow an external clock, video:setTime(music:tell()).
function NativeVideo:setTime(t)
if t < self.time or t - self.time > SEEK_THRESHOLD then
self:seek(t)
else
self.time = t
self:_advanceTo(t)
end
end
function NativeVideo:play() self.playing = true end
function NativeVideo:pause() self.playing = false end
function NativeVideo:stop() self.playing = false; self:seek(0) end
function NativeVideo:isPlaying() return self.playing end
function NativeVideo:setLoop(loop) self.loop = not not loop end
function NativeVideo:isLooping() return self.loop end
function NativeVideo:setSpeed(s) self.speed = s end
function NativeVideo:tell() return self.time end
function NativeVideo:getDuration() return self.duration end
function NativeVideo:getDimensions() return self.width, self.height end
function NativeVideo:getWidth() return self.width end
function NativeVideo:getHeight() return self.height end
function NativeVideo:getFPS() return self.fps end
function NativeVideo:getImage() return self.image end
function NativeVideo:draw(...)
love.graphics.draw(self.image, ...)
end
function NativeVideo:release()
if self.handle then
lib.nv_close(ffi.gc(self.handle, nil))
self.handle = nil
end
if self.image then self.image:release(); self.image = nil end
if self.front then self.front:release(); self.front = nil end
if self.back then self.back:release(); self.back = nil end
end
function NativeVideo.getBackend()
return ffi.string(load_library().nv_backend())
end
return NativeVideo