OnlineLibrary
385 строк · 8.4 Кб
1package player
2
3import (
4"fmt"
5"io"
6"os"
7"path/filepath"
8"strings"
9"sync"
10"sync/atomic"
11"time"
12
13"OnlineLibrary/internal/connection"
14"OnlineLibrary/internal/gui"
15"OnlineLibrary/internal/lkf"
16"OnlineLibrary/internal/log"
17"OnlineLibrary/internal/util"
18"OnlineLibrary/internal/util/buffer"
19
20"gitverse.ru/kvark128/dodp"
21)
22
23const (
24DEFAULT_SPEED = 1.0
25MIN_SPEED = 0.5
26MAX_SPEED = 3.0
27STEP_SPEED = 0.1
28
29DEFAULT_VOLUME = 0.8
30MIN_VOLUME = 0.08
31MAX_VOLUME = 1.6
32STEP_VOLUME = 0.08
33)
34
35// Extensions of supported formats
36const (
37LKF_EXT = ".lkf"
38MP3_EXT = ".mp3"
39)
40
41// Error returned when stopping playback at user request
42var PlaybackStopped = fmt.Errorf("playback stopped")
43
44type Player struct {
45logger *log.Logger
46statusBar *gui.StatusBar
47sync.Mutex
48playList []dodp.Resource
49playListSize int64
50bookDir string
51playing *atomic.Bool
52wg *sync.WaitGroup
53fragment *Fragment
54outputDevice string
55speed float64
56volume float64
57fragmentIndex int
58offset time.Duration
59timerDuration time.Duration
60pauseTimer *time.Timer
61}
62
63func NewPlayer(bookDir string, resources []dodp.Resource, outputDevice string, logger *log.Logger, statusBar *gui.StatusBar) *Player {
64p := &Player{
65logger: logger,
66statusBar: statusBar,
67playing: new(atomic.Bool),
68wg: new(sync.WaitGroup),
69bookDir: bookDir,
70speed: DEFAULT_SPEED,
71volume: DEFAULT_VOLUME,
72outputDevice: outputDevice,
73}
74
75// Player supports only LKF and MP3 resources. Unsupported resources must not be uploaded to the player
76// Some services specify an incorrect r.MimeType value, so we check the resource type by extension from the r.LocalURI field
77for _, r := range resources {
78ext := strings.ToLower(filepath.Ext(r.LocalURI))
79if ext == LKF_EXT || ext == MP3_EXT {
80p.playList = append(p.playList, r)
81p.playListSize += r.Size
82}
83}
84
85return p
86}
87
88func (p *Player) SetTimerDuration(d time.Duration) {
89p.Lock()
90defer p.Unlock()
91p.timerDuration = d
92p.logger.Debug("Playback timer set to %v", d)
93if p.playing.Load() && p.fragment != nil && !p.fragment.IsPause() {
94p.updateTimer(p.timerDuration)
95}
96}
97
98func (p *Player) TimerDuration() time.Duration {
99p.Lock()
100defer p.Unlock()
101return p.timerDuration
102}
103
104func (p *Player) updateTimer(d time.Duration) {
105if p.pauseTimer != nil {
106p.pauseTimer.Stop()
107p.pauseTimer = nil
108p.logger.Debug("Playback timer stopped")
109}
110if d > 0 {
111p.pauseTimer = time.AfterFunc(d, func() { p.Pause(true) })
112p.logger.Debug("Playback timer started on %v", d)
113}
114}
115
116func (p *Player) Position() time.Duration {
117p.Lock()
118defer p.Unlock()
119if p.fragment != nil {
120return p.fragment.Position()
121}
122return p.offset
123}
124
125func (p *Player) SetPosition(pos time.Duration) {
126p.Lock()
127defer p.Unlock()
128if !p.playing.Load() {
129p.offset = pos
130return
131}
132if p.fragment != nil {
133if err := p.fragment.SetPosition(pos); err != nil {
134p.logger.Error("Set fragment position: %v", err)
135return
136}
137p.statusBar.SetElapsedTime(p.fragment.Position())
138}
139}
140
141func (p *Player) Fragment() int {
142p.Lock()
143defer p.Unlock()
144return p.fragmentIndex
145}
146
147func (p *Player) SetFragment(fragment int) {
148p.Lock()
149defer p.Unlock()
150if fragment < 0 || fragment >= len(p.playList) {
151// This fragment does not exist. Don't need to do anything
152return
153}
154p.fragmentIndex = fragment
155if p.playing.Load() {
156p.stopPlayback()
157p.startPlayback()
158}
159}
160
161// Returns the name of the preferred audio device
162func (p *Player) OutputDevice() string {
163p.Lock()
164defer p.Unlock()
165return p.outputDevice
166}
167
168// Sets the name of the preferred audio device
169func (p *Player) SetOutputDevice(outputDevice string) {
170p.Lock()
171defer p.Unlock()
172if p.outputDevice == outputDevice {
173// The required output device is already is set
174return
175}
176p.outputDevice = outputDevice
177if p.fragment != nil {
178p.fragment.SetOutputDevice(p.outputDevice)
179}
180}
181
182func (p *Player) Speed() float64 {
183p.Lock()
184defer p.Unlock()
185return p.speed
186}
187
188func (p *Player) SetSpeed(speed float64) {
189p.Lock()
190defer p.Unlock()
191switch {
192case speed < MIN_SPEED:
193speed = MIN_SPEED
194case speed > MAX_SPEED:
195speed = MAX_SPEED
196}
197p.speed = speed
198if p.fragment != nil {
199p.fragment.setSpeed(p.speed)
200}
201}
202
203func (p *Player) Volume() float64 {
204p.Lock()
205defer p.Unlock()
206return p.volume
207}
208
209func (p *Player) SetVolume(volume float64) {
210p.Lock()
211defer p.Unlock()
212switch {
213case volume < MIN_VOLUME:
214volume = MIN_VOLUME
215case volume > MAX_VOLUME:
216volume = MAX_VOLUME
217}
218p.volume = volume
219if p.fragment != nil {
220p.fragment.setVolume(p.volume)
221}
222}
223
224func (p *Player) Pause(state bool) bool {
225p.Lock()
226defer p.Unlock()
227if !p.playing.Load() {
228if state {
229return false
230}
231p.startPlayback()
232return true
233}
234if p.fragment != nil {
235p.updateTimer(0)
236ok := p.fragment.pause(state)
237if !p.fragment.IsPause() {
238p.updateTimer(p.timerDuration)
239}
240return ok
241}
242return false
243}
244
245func (p *Player) PlayPause() {
246if !p.Pause(true) {
247p.Pause(false)
248}
249}
250
251func (p *Player) Stop() {
252p.Lock()
253defer p.Unlock()
254p.stopPlayback()
255}
256
257func (p *Player) startPlayback() {
258go p.playback(p.fragmentIndex)
259}
260
261func (p *Player) stopPlayback() {
262p.playing.Store(false)
263p.offset = 0
264if p.fragment != nil {
265p.fragment.stop()
266}
267}
268
269func (p *Player) sizeof(rsrc []dodp.Resource) int64 {
270var size int64
271for _, r := range rsrc {
272size += r.Size
273}
274return size
275}
276
277func (p *Player) playback(startFragment int) {
278p.logger.Debug("Starting playback with fragment %v. Waiting other fragments...", startFragment)
279p.wg.Wait()
280p.logger.Debug("Fragment %v started", startFragment)
281
282p.wg.Add(1)
283p.playing.Store(true)
284defer p.wg.Done()
285defer p.playing.Store(false)
286
287p.updateTimer(p.timerDuration)
288defer p.updateTimer(0)
289
290for index, r := range p.playList[startFragment:] {
291p.logger.Debug("Fetching resource: %v\r\nMimeType: %v\r\nSize: %v", r.LocalURI, r.MimeType, r.Size)
292
293err := func(r dodp.Resource) error {
294var src io.ReadSeekCloser
295localPath := filepath.Join(p.bookDir, r.LocalURI)
296
297if util.FileIsExist(localPath, r.Size) {
298// The fragment already exists on the local disk
299// We must use it to avoid making network requests
300var err error
301src, err = os.Open(localPath)
302if err != nil {
303return fmt.Errorf("getting a local fragment: %w", err)
304}
305p.logger.Debug("Opening local fragment from %v", localPath)
306} else {
307// There is no fragment on the local disc. Trying to get it from the network
308var err error
309src, err = connection.NewConnection(r.URI, p.logger)
310if err != nil {
311return fmt.Errorf("getting a remote fragment: %w", err)
312}
313p.logger.Debug("Fetching fragment by network from %v", r.URI)
314}
315defer src.Close()
316
317fragment, err := func(src io.ReadSeeker) (*Fragment, error) {
318src = buffer.NewReader(src)
319if strings.ToLower(filepath.Ext(r.LocalURI)) == LKF_EXT {
320src = lkf.NewReader(src)
321}
322
323fragment, err := NewFragment(src, p.OutputDevice())
324if err != nil {
325return nil, fmt.Errorf("creating a new fragment: %w", err)
326}
327
328if err := fragment.SetPosition(p.Position()); err != nil {
329return nil, fmt.Errorf("setting fragment position: %w", err)
330}
331
332fragment.setSpeed(p.Speed())
333fragment.setVolume(p.Volume())
334return fragment, nil
335}(src)
336
337if err != nil {
338return err
339}
340defer fragment.Close()
341
342// Fragment creation is an I/O operation and can be time consuming. We have to check that the fragment was not stopped by the user
343if !p.playing.Load() {
344return PlaybackStopped
345}
346
347p.Lock()
348p.fragment = fragment
349p.fragmentIndex = startFragment + index
350prevFragmentsSize := p.sizeof(p.playList[:p.fragmentIndex])
351byterate := int64(p.fragment.Bitrate * 1000 / 8)
352p.statusBar.SetElapsedTime(p.fragment.Position())
353p.statusBar.SetTotalTime(time.Second * time.Duration(r.Size/byterate))
354p.statusBar.SetFragments(p.fragmentIndex+1, len(p.playList))
355p.offset = 0
356p.Unlock()
357
358elapsedTimeCallback := func(d time.Duration) {
359prevSize := byterate * int64(d.Seconds())
360percent := (prevFragmentsSize + prevSize) * 100 / p.playListSize
361p.statusBar.SetElapsedTime(d)
362p.statusBar.SetBookPercent(int(percent))
363}
364
365err = fragment.play(p.playing, elapsedTimeCallback)
366p.Lock()
367p.fragment = nil
368p.Unlock()
369
370if err != nil {
371return fmt.Errorf("playing a fragment: %w", err)
372}
373
374if !p.playing.Load() {
375return PlaybackStopped
376}
377return nil
378}(r)
379
380if err != nil {
381p.logger.Warning("Resource %v: %v", r.LocalURI, err)
382break
383}
384}
385}
386