OnlineLibrary

Форк
0
385 строк · 8.4 Кб
1
package player
2

3
import (
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

23
const (
24
	DEFAULT_SPEED = 1.0
25
	MIN_SPEED     = 0.5
26
	MAX_SPEED     = 3.0
27
	STEP_SPEED    = 0.1
28

29
	DEFAULT_VOLUME = 0.8
30
	MIN_VOLUME     = 0.08
31
	MAX_VOLUME     = 1.6
32
	STEP_VOLUME    = 0.08
33
)
34

35
// Extensions of supported formats
36
const (
37
	LKF_EXT = ".lkf"
38
	MP3_EXT = ".mp3"
39
)
40

41
// Error returned when stopping playback at user request
42
var PlaybackStopped = fmt.Errorf("playback stopped")
43

44
type Player struct {
45
	logger    *log.Logger
46
	statusBar *gui.StatusBar
47
	sync.Mutex
48
	playList      []dodp.Resource
49
	playListSize  int64
50
	bookDir       string
51
	playing       *atomic.Bool
52
	wg            *sync.WaitGroup
53
	fragment      *Fragment
54
	outputDevice  string
55
	speed         float64
56
	volume        float64
57
	fragmentIndex int
58
	offset        time.Duration
59
	timerDuration time.Duration
60
	pauseTimer    *time.Timer
61
}
62

63
func NewPlayer(bookDir string, resources []dodp.Resource, outputDevice string, logger *log.Logger, statusBar *gui.StatusBar) *Player {
64
	p := &Player{
65
		logger:       logger,
66
		statusBar:    statusBar,
67
		playing:      new(atomic.Bool),
68
		wg:           new(sync.WaitGroup),
69
		bookDir:      bookDir,
70
		speed:        DEFAULT_SPEED,
71
		volume:       DEFAULT_VOLUME,
72
		outputDevice: 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
77
	for _, r := range resources {
78
		ext := strings.ToLower(filepath.Ext(r.LocalURI))
79
		if ext == LKF_EXT || ext == MP3_EXT {
80
			p.playList = append(p.playList, r)
81
			p.playListSize += r.Size
82
		}
83
	}
84

85
	return p
86
}
87

88
func (p *Player) SetTimerDuration(d time.Duration) {
89
	p.Lock()
90
	defer p.Unlock()
91
	p.timerDuration = d
92
	p.logger.Debug("Playback timer set to %v", d)
93
	if p.playing.Load() && p.fragment != nil && !p.fragment.IsPause() {
94
		p.updateTimer(p.timerDuration)
95
	}
96
}
97

98
func (p *Player) TimerDuration() time.Duration {
99
	p.Lock()
100
	defer p.Unlock()
101
	return p.timerDuration
102
}
103

104
func (p *Player) updateTimer(d time.Duration) {
105
	if p.pauseTimer != nil {
106
		p.pauseTimer.Stop()
107
		p.pauseTimer = nil
108
		p.logger.Debug("Playback timer stopped")
109
	}
110
	if d > 0 {
111
		p.pauseTimer = time.AfterFunc(d, func() { p.Pause(true) })
112
		p.logger.Debug("Playback timer started on %v", d)
113
	}
114
}
115

116
func (p *Player) Position() time.Duration {
117
	p.Lock()
118
	defer p.Unlock()
119
	if p.fragment != nil {
120
		return p.fragment.Position()
121
	}
122
	return p.offset
123
}
124

125
func (p *Player) SetPosition(pos time.Duration) {
126
	p.Lock()
127
	defer p.Unlock()
128
	if !p.playing.Load() {
129
		p.offset = pos
130
		return
131
	}
132
	if p.fragment != nil {
133
		if err := p.fragment.SetPosition(pos); err != nil {
134
			p.logger.Error("Set fragment position: %v", err)
135
			return
136
		}
137
		p.statusBar.SetElapsedTime(p.fragment.Position())
138
	}
139
}
140

141
func (p *Player) Fragment() int {
142
	p.Lock()
143
	defer p.Unlock()
144
	return p.fragmentIndex
145
}
146

147
func (p *Player) SetFragment(fragment int) {
148
	p.Lock()
149
	defer p.Unlock()
150
	if fragment < 0 || fragment >= len(p.playList) {
151
		// This fragment does not exist. Don't need to do anything
152
		return
153
	}
154
	p.fragmentIndex = fragment
155
	if p.playing.Load() {
156
		p.stopPlayback()
157
		p.startPlayback()
158
	}
159
}
160

161
// Returns the name of the preferred audio device
162
func (p *Player) OutputDevice() string {
163
	p.Lock()
164
	defer p.Unlock()
165
	return p.outputDevice
166
}
167

168
// Sets the name of the preferred audio device
169
func (p *Player) SetOutputDevice(outputDevice string) {
170
	p.Lock()
171
	defer p.Unlock()
172
	if p.outputDevice == outputDevice {
173
		// The required output device is already is set
174
		return
175
	}
176
	p.outputDevice = outputDevice
177
	if p.fragment != nil {
178
		p.fragment.SetOutputDevice(p.outputDevice)
179
	}
180
}
181

182
func (p *Player) Speed() float64 {
183
	p.Lock()
184
	defer p.Unlock()
185
	return p.speed
186
}
187

188
func (p *Player) SetSpeed(speed float64) {
189
	p.Lock()
190
	defer p.Unlock()
191
	switch {
192
	case speed < MIN_SPEED:
193
		speed = MIN_SPEED
194
	case speed > MAX_SPEED:
195
		speed = MAX_SPEED
196
	}
197
	p.speed = speed
198
	if p.fragment != nil {
199
		p.fragment.setSpeed(p.speed)
200
	}
201
}
202

203
func (p *Player) Volume() float64 {
204
	p.Lock()
205
	defer p.Unlock()
206
	return p.volume
207
}
208

209
func (p *Player) SetVolume(volume float64) {
210
	p.Lock()
211
	defer p.Unlock()
212
	switch {
213
	case volume < MIN_VOLUME:
214
		volume = MIN_VOLUME
215
	case volume > MAX_VOLUME:
216
		volume = MAX_VOLUME
217
	}
218
	p.volume = volume
219
	if p.fragment != nil {
220
		p.fragment.setVolume(p.volume)
221
	}
222
}
223

224
func (p *Player) Pause(state bool) bool {
225
	p.Lock()
226
	defer p.Unlock()
227
	if !p.playing.Load() {
228
		if state {
229
			return false
230
		}
231
		p.startPlayback()
232
		return true
233
	}
234
	if p.fragment != nil {
235
		p.updateTimer(0)
236
		ok := p.fragment.pause(state)
237
		if !p.fragment.IsPause() {
238
			p.updateTimer(p.timerDuration)
239
		}
240
		return ok
241
	}
242
	return false
243
}
244

245
func (p *Player) PlayPause() {
246
	if !p.Pause(true) {
247
		p.Pause(false)
248
	}
249
}
250

251
func (p *Player) Stop() {
252
	p.Lock()
253
	defer p.Unlock()
254
	p.stopPlayback()
255
}
256

257
func (p *Player) startPlayback() {
258
	go p.playback(p.fragmentIndex)
259
}
260

261
func (p *Player) stopPlayback() {
262
	p.playing.Store(false)
263
	p.offset = 0
264
	if p.fragment != nil {
265
		p.fragment.stop()
266
	}
267
}
268

269
func (p *Player) sizeof(rsrc []dodp.Resource) int64 {
270
	var size int64
271
	for _, r := range rsrc {
272
		size += r.Size
273
	}
274
	return size
275
}
276

277
func (p *Player) playback(startFragment int) {
278
	p.logger.Debug("Starting playback with fragment %v. Waiting other fragments...", startFragment)
279
	p.wg.Wait()
280
	p.logger.Debug("Fragment %v started", startFragment)
281

282
	p.wg.Add(1)
283
	p.playing.Store(true)
284
	defer p.wg.Done()
285
	defer p.playing.Store(false)
286

287
	p.updateTimer(p.timerDuration)
288
	defer p.updateTimer(0)
289

290
	for index, r := range p.playList[startFragment:] {
291
		p.logger.Debug("Fetching resource: %v\r\nMimeType: %v\r\nSize: %v", r.LocalURI, r.MimeType, r.Size)
292

293
		err := func(r dodp.Resource) error {
294
			var src io.ReadSeekCloser
295
			localPath := filepath.Join(p.bookDir, r.LocalURI)
296

297
			if util.FileIsExist(localPath, r.Size) {
298
				// The fragment already exists on the local disk
299
				// We must use it to avoid making network requests
300
				var err error
301
				src, err = os.Open(localPath)
302
				if err != nil {
303
					return fmt.Errorf("getting a local fragment: %w", err)
304
				}
305
				p.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
308
				var err error
309
				src, err = connection.NewConnection(r.URI, p.logger)
310
				if err != nil {
311
					return fmt.Errorf("getting a remote fragment: %w", err)
312
				}
313
				p.logger.Debug("Fetching fragment by network from %v", r.URI)
314
			}
315
			defer src.Close()
316

317
			fragment, err := func(src io.ReadSeeker) (*Fragment, error) {
318
				src = buffer.NewReader(src)
319
				if strings.ToLower(filepath.Ext(r.LocalURI)) == LKF_EXT {
320
					src = lkf.NewReader(src)
321
				}
322

323
				fragment, err := NewFragment(src, p.OutputDevice())
324
				if err != nil {
325
					return nil, fmt.Errorf("creating a new fragment: %w", err)
326
				}
327

328
				if err := fragment.SetPosition(p.Position()); err != nil {
329
					return nil, fmt.Errorf("setting fragment position: %w", err)
330
				}
331

332
				fragment.setSpeed(p.Speed())
333
				fragment.setVolume(p.Volume())
334
				return fragment, nil
335
			}(src)
336

337
			if err != nil {
338
				return err
339
			}
340
			defer 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
343
			if !p.playing.Load() {
344
				return PlaybackStopped
345
			}
346

347
			p.Lock()
348
			p.fragment = fragment
349
			p.fragmentIndex = startFragment + index
350
			prevFragmentsSize := p.sizeof(p.playList[:p.fragmentIndex])
351
			byterate := int64(p.fragment.Bitrate * 1000 / 8)
352
			p.statusBar.SetElapsedTime(p.fragment.Position())
353
			p.statusBar.SetTotalTime(time.Second * time.Duration(r.Size/byterate))
354
			p.statusBar.SetFragments(p.fragmentIndex+1, len(p.playList))
355
			p.offset = 0
356
			p.Unlock()
357

358
			elapsedTimeCallback := func(d time.Duration) {
359
				prevSize := byterate * int64(d.Seconds())
360
				percent := (prevFragmentsSize + prevSize) * 100 / p.playListSize
361
				p.statusBar.SetElapsedTime(d)
362
				p.statusBar.SetBookPercent(int(percent))
363
			}
364

365
			err = fragment.play(p.playing, elapsedTimeCallback)
366
			p.Lock()
367
			p.fragment = nil
368
			p.Unlock()
369

370
			if err != nil {
371
				return fmt.Errorf("playing a fragment: %w", err)
372
			}
373

374
			if !p.playing.Load() {
375
				return PlaybackStopped
376
			}
377
			return nil
378
		}(r)
379

380
		if err != nil {
381
			p.logger.Warning("Resource %v: %v", r.LocalURI, err)
382
			break
383
		}
384
	}
385
}
386

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.