mirror of
https://github.com/AynaLivePlayer/AynaLivePlayer.git
synced 2025-12-15 06:28:18 +08:00
Initial commit
This commit is contained in:
38
player/event.go
Normal file
38
player/event.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/event"
|
||||
)
|
||||
|
||||
const (
|
||||
EventPlay event.EventId = "player.play"
|
||||
EventPlaylistPreInsert event.EventId = "playlist.insert.pre"
|
||||
EventPlaylistInsert event.EventId = "playlist.insert.after"
|
||||
EventPlaylistUpdate event.EventId = "playlist.update"
|
||||
EventLyricUpdate event.EventId = "lyric.update"
|
||||
EventLyricReload event.EventId = "lyric.reload"
|
||||
)
|
||||
|
||||
type PlaylistInsertEvent struct {
|
||||
Playlist *Playlist
|
||||
Index int
|
||||
Media *Media
|
||||
}
|
||||
|
||||
type PlaylistUpdateEvent struct {
|
||||
Playlist *Playlist
|
||||
}
|
||||
|
||||
type PlayEvent struct {
|
||||
Media *Media
|
||||
}
|
||||
|
||||
type LyricUpdateEvent struct {
|
||||
Lyrics *Lyric
|
||||
Time float64
|
||||
Lyric *LyricLine
|
||||
}
|
||||
|
||||
type LyricReloadEvent struct {
|
||||
Lyrics *Lyric
|
||||
}
|
||||
89
player/lyric.go
Normal file
89
player/lyric.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/event"
|
||||
"github.com/spf13/cast"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var timeTagRegex = regexp.MustCompile("\\[[0-9]+:[0-9]+(\\.[0-9]+)?\\]")
|
||||
|
||||
type LyricLine struct {
|
||||
Time float64 // in seconds
|
||||
Lyric string
|
||||
}
|
||||
|
||||
type Lyric struct {
|
||||
Lyrics []LyricLine
|
||||
Handler *event.Handler
|
||||
}
|
||||
|
||||
func (l *Lyric) Reload(lyric string) {
|
||||
tmp := make(map[float64]LyricLine)
|
||||
times := make([]float64, 0)
|
||||
for _, line := range strings.Split(lyric, "\n") {
|
||||
lrc := timeTagRegex.ReplaceAllString(line, "")
|
||||
for _, time := range timeTagRegex.FindAllString(line, -1) {
|
||||
ts := strings.Split(time[1:len(time)-1], ":")
|
||||
t := cast.ToFloat64(ts[0])*60 + cast.ToFloat64(ts[1])
|
||||
times = append(times, t)
|
||||
tmp[t] = LyricLine{
|
||||
Time: t,
|
||||
Lyric: lrc,
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Float64s(times)
|
||||
lrcs := make([]LyricLine, len(times))
|
||||
for index, time := range times {
|
||||
lrcs[index] = tmp[time]
|
||||
}
|
||||
lrcs = append(lrcs, LyricLine{
|
||||
Time: 99999999999,
|
||||
Lyric: "",
|
||||
})
|
||||
l.Lyrics = lrcs
|
||||
l.Handler.CallA(EventLyricReload, LyricReloadEvent{Lyrics: l})
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lyric) Update(time float64) {
|
||||
l.Handler.CallA(EventLyricUpdate, LyricUpdateEvent{
|
||||
Lyrics: l,
|
||||
Time: time,
|
||||
Lyric: l.Find(time),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Lyric) Find(time float64) *LyricLine {
|
||||
for i := 0; i < len(l.Lyrics)-1; i++ {
|
||||
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
|
||||
return &l.Lyrics[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Lyric) FindContext(time float64, prev int, next int) []LyricLine {
|
||||
for i := 0; i < len(l.Lyrics)-1; i++ {
|
||||
if l.Lyrics[i].Time <= time && time < l.Lyrics[i+1].Time {
|
||||
if (i + prev) < 0 {
|
||||
prev = -i
|
||||
}
|
||||
if (i + 1 + next) > len(l.Lyrics) {
|
||||
next = len(l.Lyrics) - i - 1
|
||||
}
|
||||
return l.Lyrics[i+prev : i+1+next]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewLyric(lyric string) *Lyric {
|
||||
l := &Lyric{Handler: event.NewHandler()}
|
||||
l.Reload(lyric)
|
||||
return l
|
||||
}
|
||||
23
player/lyric_test.go
Normal file
23
player/lyric_test.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testLyric = "[ti:双截棍]\n[ar:周杰伦]\n[al:范特西]\n[00:03.85]双截棍\n[00:07.14]\n[00:30.13]岩烧店的烟味弥漫隔壁是国术馆\n[00:32.57]店里面的妈妈桑茶道有三段\n[00:34.61]教拳脚武术的老板练铁沙掌耍杨家枪\n[00:37.34]硬底子功夫最擅长还会金钟罩铁步衫\n[00:39.67]他们儿子我习惯从小就耳濡目染\n[00:41.96]什么刀枪跟棍棒我都耍的有模有样\n[00:44.22]什么兵器最喜欢双截棍柔中带刚\n[00:46.73]想要去河南嵩山学少林跟武当\n[00:49.24]干什么(客)干什么(客)呼吸吐纳心自在\n[00:51.28]干什么(客)干什么(客)气沉丹田手心开\n[00:53.44]干什么(客)干什么(客)日行千里系沙袋\n[00:56.13]飞檐走壁莫奇怪去去就来\n[00:58.35]一个马步向前一记左钩拳右钩拳\n[01:01.26]一句惹毛我的人有危险一再重演\n[01:04.02]一根我不抽的菸一放好多年它一直在身边\n[01:07.28]干什么(客)干什么(客)我打开任督二脉\n[01:10.27]干什么(客)干什么(客)东亚病夫的招牌\n[01:12.75]干什么(客)干什么(客)已被我一脚踢开\n[02:32.62][01:54.69][01:15.40]快使用双截棍哼哼哈兮\n[02:34.52][01:56.63][01:18.40]快使用双截棍哼哼哈兮\n[02:36.88][01:58.98][01:20.71]习武之人切记仁者无敌\n[02:39.45][02:01.66][01:23.27]是谁在练太极风生水起\n[02:41.97][02:03.93][01:25.74]快使用双截棍哼哼哈兮\n[02:44.42][02:06.11][01:27.75]快使用双截棍哼哼哈兮\n[02:47.01][02:08.54][01:30.13]如果我有轻功飞檐走壁\n[02:49.36][02:11.03][01:32.67]为人耿直不屈一身正气\n[02:53.81]快使用双截棍哼\n[02:56.30]我用手刀防御哼\n[02:58.52]漂亮的回旋踢\n[02:59.52]"
|
||||
|
||||
func TestLyric(t *testing.T) {
|
||||
lryic := NewLyric(testLyric)
|
||||
for _, lrc := range lryic.Lyrics {
|
||||
fmt.Println(lrc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLyricFind(t *testing.T) {
|
||||
lryic := NewLyric(testLyric)
|
||||
fmt.Println(lryic.Find(90.4))
|
||||
for _, l := range lryic.FindContext(90.4, -2, 2) {
|
||||
fmt.Println(l)
|
||||
}
|
||||
}
|
||||
36
player/media.go
Normal file
36
player/media.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package player
|
||||
|
||||
import "AynaLivePlayer/liveclient"
|
||||
|
||||
type Media struct {
|
||||
Title string
|
||||
Artist string
|
||||
Cover string
|
||||
Album string
|
||||
Lyric string
|
||||
Url string
|
||||
Header map[string]string
|
||||
User interface{}
|
||||
Meta interface{}
|
||||
}
|
||||
|
||||
func (m *Media) ToUser() *User {
|
||||
if u, ok := m.User.(*User); ok {
|
||||
return u
|
||||
}
|
||||
return &User{Name: m.DanmuUser().Username}
|
||||
}
|
||||
|
||||
func (m *Media) SystemUser() *User {
|
||||
if u, ok := m.User.(*User); ok {
|
||||
return u
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Media) DanmuUser() *liveclient.DanmuUser {
|
||||
if u, ok := m.User.(*liveclient.DanmuUser); ok {
|
||||
return u
|
||||
}
|
||||
return nil
|
||||
}
|
||||
22
player/media_test.go
Normal file
22
player/media_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type A struct {
|
||||
A string
|
||||
}
|
||||
|
||||
type B struct {
|
||||
B string
|
||||
}
|
||||
|
||||
func TestStruct(t *testing.T) {
|
||||
var x interface{} = &A{A: "123"}
|
||||
y, ok := x.(*A)
|
||||
fmt.Println(y, ok)
|
||||
z, ok := x.(*B)
|
||||
fmt.Println(z, ok)
|
||||
}
|
||||
BIN
player/mpv-2.dll
Normal file
BIN
player/mpv-2.dll
Normal file
Binary file not shown.
156
player/player.go
Normal file
156
player/player.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/event"
|
||||
"AynaLivePlayer/logger"
|
||||
"AynaLivePlayer/util"
|
||||
"github.com/aynakeya/go-mpv"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const MODULE_PLAYER = "Player.Player"
|
||||
|
||||
type PropertyHandlerFunc func(property *mpv.EventProperty)
|
||||
|
||||
type Player struct {
|
||||
running bool
|
||||
libmpv *mpv.Mpv
|
||||
Playing *Media
|
||||
PropertyHandler map[string][]PropertyHandlerFunc
|
||||
EventHandler *event.Handler
|
||||
}
|
||||
|
||||
func NewPlayer() *Player {
|
||||
player := &Player{
|
||||
running: true,
|
||||
libmpv: mpv.Create(),
|
||||
PropertyHandler: make(map[string][]PropertyHandlerFunc),
|
||||
EventHandler: event.NewHandler(),
|
||||
}
|
||||
err := player.libmpv.Initialize()
|
||||
if err != nil {
|
||||
player.l().Error("initialize libmpv failed")
|
||||
return nil
|
||||
}
|
||||
player.libmpv.SetOptionString("vo", "null")
|
||||
player.l().Info("initialize libmpv success")
|
||||
return player
|
||||
}
|
||||
|
||||
func (p *Player) Start() {
|
||||
p.l().Info("starting mpv player")
|
||||
go func() {
|
||||
for p.running {
|
||||
e := p.libmpv.WaitEvent(1)
|
||||
if e == nil {
|
||||
p.l().Warn("event loop got nil event")
|
||||
}
|
||||
p.l().Trace("new event", e)
|
||||
if e.EventId == mpv.EVENT_PROPERTY_CHANGE {
|
||||
property := e.Property()
|
||||
p.l().Trace("receive property change event", property)
|
||||
for _, handler := range p.PropertyHandler[property.Name] {
|
||||
// todo: @3
|
||||
go handler(&property)
|
||||
}
|
||||
}
|
||||
if e.EventId == mpv.EVENT_SHUTDOWN {
|
||||
p.l().Info("libmpv shutdown")
|
||||
p.Stop()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Player) Stop() {
|
||||
p.l().Info("stopping mpv player")
|
||||
p.running = false
|
||||
p.libmpv.TerminateDestroy()
|
||||
}
|
||||
|
||||
func (p *Player) l() *logrus.Entry {
|
||||
return logger.Logger.WithField("Module", MODULE_PLAYER)
|
||||
}
|
||||
|
||||
func (p *Player) Play(media *Media) error {
|
||||
p.l().Infof("Play media %s", media.Url)
|
||||
p.l().Trace("set user-agent for mpv player")
|
||||
if val, ok := media.Header["user-agent"]; ok {
|
||||
err := p.libmpv.SetPropertyString("user-agent", val)
|
||||
if err != nil {
|
||||
p.l().Warn("set player user-agent failed", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.l().Trace("set referrer for mpv player")
|
||||
if val, ok := media.Header["referrer"]; ok {
|
||||
err := p.libmpv.SetPropertyString("referrer", val)
|
||||
if err != nil {
|
||||
p.l().Warn("set player referrer failed", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
p.l().Debug("mpv command load file", media)
|
||||
if err := p.libmpv.Command([]string{"loadfile", media.Url}); err != nil {
|
||||
p.l().Warn("mpv load media failed", media)
|
||||
return err
|
||||
}
|
||||
p.Playing = media
|
||||
p.EventHandler.CallA(EventPlay, PlayEvent{Media: media})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Player) IsPaused() bool {
|
||||
property, err := p.libmpv.GetProperty("pause", mpv.FORMAT_FLAG)
|
||||
if err != nil {
|
||||
p.l().Warn("get property pause failed", err)
|
||||
return false
|
||||
}
|
||||
return property.(bool)
|
||||
}
|
||||
|
||||
func (p *Player) Pause() error {
|
||||
p.l().Tracef("pause")
|
||||
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, true)
|
||||
}
|
||||
|
||||
func (p *Player) Unpause() error {
|
||||
p.l().Tracef("unpause")
|
||||
return p.libmpv.SetProperty("pause", mpv.FORMAT_FLAG, false)
|
||||
}
|
||||
|
||||
// SetVolume set mpv volume, from 0.0 - 100.0
|
||||
func (p *Player) SetVolume(volume float64) error {
|
||||
p.l().Tracef("set volume to %f", volume)
|
||||
return p.libmpv.SetProperty("volume", mpv.FORMAT_DOUBLE, volume)
|
||||
}
|
||||
|
||||
func (p *Player) IsIdle() bool {
|
||||
property, err := p.libmpv.GetProperty("idle-active", mpv.FORMAT_FLAG)
|
||||
if err != nil {
|
||||
p.l().Warn("get property idle-active failed", err)
|
||||
return false
|
||||
}
|
||||
return property.(bool)
|
||||
}
|
||||
|
||||
// Seek change position for current file
|
||||
// absolute = true : position is the time in second
|
||||
// absolute = false: position is in percentage eg 0.1 0.2
|
||||
func (p *Player) Seek(position float64, absolute bool) error {
|
||||
p.l().Tracef("seek to %f (absolute=%t)", position, absolute)
|
||||
if absolute {
|
||||
return p.libmpv.SetProperty("time-pos", mpv.FORMAT_DOUBLE, position)
|
||||
} else {
|
||||
return p.libmpv.SetProperty("percent-pos", mpv.FORMAT_DOUBLE, position)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) ObserveProperty(property string, handler ...PropertyHandlerFunc) error {
|
||||
p.l().Trace("add property observer for mpv")
|
||||
p.PropertyHandler[property] = append(p.PropertyHandler[property], handler...)
|
||||
if len(p.PropertyHandler[property]) == 1 {
|
||||
return p.libmpv.ObserveProperty(util.Hash64(property), property, mpv.FORMAT_NODE)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
25
player/player_test.go
Normal file
25
player/player_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/aynakeya/go-mpv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPlayer(t *testing.T) {
|
||||
player := NewPlayer()
|
||||
player.Start()
|
||||
defer player.Stop()
|
||||
|
||||
player.ObserveProperty("time-pos", func(property *mpv.EventProperty) {
|
||||
fmt.Println(1, property.Data)
|
||||
})
|
||||
player.ObserveProperty("percent-pos", func(property *mpv.EventProperty) {
|
||||
fmt.Println(2, property.Data)
|
||||
})
|
||||
player.Play(&Media{
|
||||
Url: "https://ia600809.us.archive.org/19/items/VillagePeopleYMCAOFFICIALMusicVideo1978/Village%20People%20-%20YMCA%20OFFICIAL%20Music%20Video%201978.mp4",
|
||||
})
|
||||
time.Sleep(time.Second * 15)
|
||||
}
|
||||
144
player/playlist.go
Normal file
144
player/playlist.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"AynaLivePlayer/event"
|
||||
"AynaLivePlayer/logger"
|
||||
"github.com/sirupsen/logrus"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const MODULE_PLAYLIST = "Player.Playlist"
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
type PlaylistConfig struct {
|
||||
RandomNext bool
|
||||
}
|
||||
|
||||
type Playlist struct {
|
||||
Index int
|
||||
Name string
|
||||
Config PlaylistConfig
|
||||
Playlist []*Media
|
||||
Handler *event.Handler
|
||||
Meta interface{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewPlaylist(name string, config PlaylistConfig) *Playlist {
|
||||
return &Playlist{
|
||||
Index: 0,
|
||||
Name: name,
|
||||
Config: config,
|
||||
Playlist: make([]*Media, 0),
|
||||
Handler: event.NewHandler(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Playlist) l() *logrus.Entry {
|
||||
return logger.Logger.WithFields(logrus.Fields{
|
||||
"Module": MODULE_PLAYLIST,
|
||||
"Name": p.Name,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Playlist) Size() int {
|
||||
p.l().Tracef("getting size=%d", len(p.Playlist))
|
||||
return len(p.Playlist)
|
||||
}
|
||||
|
||||
func (p *Playlist) Pop() *Media {
|
||||
p.l().Infof("pop first media")
|
||||
if p.Size() == 0 {
|
||||
p.l().Warn("pop first media failed, no media left in the playlist")
|
||||
return nil
|
||||
}
|
||||
p.lock.Lock()
|
||||
media := p.Playlist[0]
|
||||
p.Playlist = p.Playlist[1:]
|
||||
p.lock.Unlock()
|
||||
defer p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
||||
return media
|
||||
}
|
||||
|
||||
func (p *Playlist) Replace(medias []*Media) {
|
||||
p.lock.Lock()
|
||||
p.Playlist = medias
|
||||
p.Index = 0
|
||||
p.lock.Unlock()
|
||||
p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Playlist) Push(media *Media) {
|
||||
p.Insert(-1, media)
|
||||
defer p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
||||
return
|
||||
}
|
||||
|
||||
// Insert runtime in O(n) but i don't care
|
||||
func (p *Playlist) Insert(index int, media *Media) {
|
||||
p.l().Infof("insert new meida to index %d", index)
|
||||
p.l().Debug("media=", *media)
|
||||
e := event.Event{
|
||||
Id: EventPlaylistPreInsert,
|
||||
Cancelled: false,
|
||||
Data: PlaylistInsertEvent{
|
||||
Playlist: p,
|
||||
Index: index,
|
||||
Media: media,
|
||||
},
|
||||
}
|
||||
p.Handler.Call(&e)
|
||||
if e.Cancelled {
|
||||
p.l().Info("insert new media has been cancelled by handler")
|
||||
return
|
||||
}
|
||||
p.lock.Lock()
|
||||
if index > p.Size() {
|
||||
index = p.Size()
|
||||
}
|
||||
if index < 0 {
|
||||
index = p.Size() + index + 1
|
||||
}
|
||||
p.Playlist = append(p.Playlist, nil)
|
||||
for i := p.Size() - 1; i > index; i-- {
|
||||
p.Playlist[i] = p.Playlist[i-1]
|
||||
}
|
||||
p.Playlist[index] = media
|
||||
p.lock.Unlock()
|
||||
defer func() {
|
||||
p.Handler.Call(&event.Event{
|
||||
Id: EventPlaylistInsert,
|
||||
Cancelled: false,
|
||||
Data: PlaylistInsertEvent{
|
||||
Playlist: p,
|
||||
Index: index,
|
||||
Media: media,
|
||||
},
|
||||
})
|
||||
p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Playlist) Next() *Media {
|
||||
p.l().Infof("get next media with random=%t", p.Config.RandomNext)
|
||||
if p.Size() == 0 {
|
||||
p.l().Info("get next media failed, no media left in the playlist")
|
||||
return nil
|
||||
}
|
||||
var index int
|
||||
index = p.Index
|
||||
if p.Config.RandomNext {
|
||||
p.Index = rand.Intn(p.Size())
|
||||
} else {
|
||||
p.Index = (p.Index + 1) % p.Size()
|
||||
}
|
||||
p.l().Tracef("return index %d, new index %d", index, p.Index)
|
||||
defer p.Handler.CallA(EventPlaylistUpdate, PlaylistUpdateEvent{Playlist: p})
|
||||
return p.Playlist[index]
|
||||
}
|
||||
22
player/playlist_test.go
Normal file
22
player/playlist_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlaylist_Insert(t *testing.T) {
|
||||
pl := NewPlaylist("asdf", PlaylistConfig{RandomNext: false})
|
||||
for i := 0; i < 10; i++ {
|
||||
pl.Insert(-1, &Media{Url: strconv.Itoa(i)})
|
||||
}
|
||||
pl.Insert(3, &Media{Url: "a"})
|
||||
pl.Insert(0, &Media{Url: "b"})
|
||||
pl.Insert(-2, &Media{Url: "x"})
|
||||
pl.Insert(-1, &Media{Url: "h"})
|
||||
for i := 0; i < pl.Size(); i++ {
|
||||
fmt.Print(pl.Playlist[i].Url, " ")
|
||||
}
|
||||
|
||||
}
|
||||
8
player/user.go
Normal file
8
player/user.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package player
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
var PlaylistUser = &User{Name: "Playlist"}
|
||||
var SystemUser = &User{Name: "System"}
|
||||
Reference in New Issue
Block a user