prepare play
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"msw-open-music/internal/pkg/database"
|
"msw-open-music/internal/pkg/database"
|
||||||
|
"msw-open-music/internal/pkg/tmpfs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -20,6 +21,7 @@ type API struct {
|
|||||||
Server http.Server
|
Server http.Server
|
||||||
token string
|
token string
|
||||||
APIConfig APIConfig
|
APIConfig APIConfig
|
||||||
|
Tmpfs *tmpfs.Tmpfs
|
||||||
}
|
}
|
||||||
|
|
||||||
type FfmpegConfigs struct {
|
type FfmpegConfigs struct {
|
||||||
@@ -345,23 +347,39 @@ func (api *API) HandleGetFileInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
func (api *API) CheckGetFileStream(w http.ResponseWriter, r *http.Request) (error) {
|
||||||
|
var err error
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
ids := q["id"]
|
ids := q["id"]
|
||||||
if len(ids) == 0 {
|
if len(ids) == 0 {
|
||||||
api.HandleErrorString(w, r, `parameter "id" can't be empty`)
|
err = errors.New(`parameter "id" can't be empty`)
|
||||||
return
|
api.HandleError(w, r, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
id, err := strconv.Atoi(ids[0])
|
_, err = strconv.Atoi(ids[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.HandleErrorString(w, r, `parameter "id" should be an integer`)
|
err = errors.New(`parameter "id" should be an integer`)
|
||||||
return
|
api.HandleError(w, r, err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
configs := q["config"]
|
configs := q["config"]
|
||||||
if len(configs) == 0 {
|
if len(configs) == 0 {
|
||||||
api.HandleErrorString(w, r, `parameter "config" can't be empty`)
|
err = errors.New(`parameter "config" can't be empty`)
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := api.CheckGetFileStream(w, r)
|
||||||
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
id, err := strconv.Atoi(ids[0])
|
||||||
|
configs := q["config"]
|
||||||
configName := configs[0]
|
configName := configs[0]
|
||||||
file, err := api.Db.GetFile(int64(id))
|
file, err := api.Db.GetFile(int64(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -384,7 +402,7 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
args := strings.Split(ffmpegConfig.Args, " ")
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
startArgs := []string {"-i", path}
|
startArgs := []string {"-i", path}
|
||||||
endArgs := []string {"-vn", "-f", "matroska", "-"}
|
endArgs := []string {"-vn", "-f", "ogg", "-"}
|
||||||
ffmpegArgs := append(startArgs, args...)
|
ffmpegArgs := append(startArgs, args...)
|
||||||
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||||
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||||
@@ -396,6 +414,111 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PrepareFileStreamDirectRequest struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ConfigName string `json:"config_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrepareFileStreamDirectResponse struct {
|
||||||
|
Filesize int64 `json:"filesize"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandlePrepareFileStreamDirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
prepareFileStreamDirectRequst := &PrepareFileStreamDirectRequest{
|
||||||
|
ID: -1,
|
||||||
|
}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(prepareFileStreamDirectRequst)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check empty
|
||||||
|
if prepareFileStreamDirectRequst.ID < 0 {
|
||||||
|
api.HandleErrorString(w, r, `"id" can't be none or negative`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if prepareFileStreamDirectRequst.ConfigName == "" {
|
||||||
|
api.HandleErrorString(w, r, `"config_name" can't be empty`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := api.Db.GetFile(prepareFileStreamDirectRequst.ID)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srcPath, err := file.Path()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[api] Prepare stream direct file", srcPath, prepareFileStreamDirectRequst.ConfigName)
|
||||||
|
ffmpegConfig, ok := api.APIConfig.FfmpegConfigs[prepareFileStreamDirectRequst.ConfigName]
|
||||||
|
if !ok {
|
||||||
|
api.HandleErrorStringCode(w, r, `ffmpeg config not found`, 404)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
objPath := api.Tmpfs.GetObjFilePath(prepareFileStreamDirectRequst.ID, prepareFileStreamDirectRequst.ConfigName)
|
||||||
|
|
||||||
|
// check obj file exists
|
||||||
|
exists := api.Tmpfs.Exits(objPath)
|
||||||
|
if exists {
|
||||||
|
fileInfo, err := os.Stat(objPath)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||||
|
Filesize: fileInfo.Size(),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.Tmpfs.Record(objPath)
|
||||||
|
args := strings.Split(ffmpegConfig.Args, " ")
|
||||||
|
startArgs := []string {"-i", srcPath}
|
||||||
|
endArgs := []string {"-vn", "-y", objPath}
|
||||||
|
ffmpegArgs := append(startArgs, args...)
|
||||||
|
ffmpegArgs = append(ffmpegArgs, endArgs...)
|
||||||
|
cmd := exec.Command("ffmpeg", ffmpegArgs...)
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo, err := os.Stat(objPath)
|
||||||
|
if err != nil {
|
||||||
|
api.HandleError(w, r, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prepareFileStreamDirectResponse := &PrepareFileStreamDirectResponse{
|
||||||
|
Filesize: fileInfo.Size(),
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(prepareFileStreamDirectResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) HandleGetFileStreamDirect(w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := api.CheckGetFileStream(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := r.URL.Query()
|
||||||
|
ids := q["id"]
|
||||||
|
id, err := strconv.Atoi(ids[0])
|
||||||
|
configs := q["config"]
|
||||||
|
configName := configs[0]
|
||||||
|
|
||||||
|
path := api.Tmpfs.GetObjFilePath(int64(id), configName)
|
||||||
|
|
||||||
|
log.Println("[api] Get direct cached file", path)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, path)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) {
|
func (api *API) HandleGetFileDirect(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query()
|
q := r.URL.Query()
|
||||||
ids := q["id"]
|
ids := q["id"]
|
||||||
@@ -572,6 +695,7 @@ func NewAPI(apiConfig APIConfig) (*API, error) {
|
|||||||
},
|
},
|
||||||
APIConfig: apiConfig,
|
APIConfig: apiConfig,
|
||||||
}
|
}
|
||||||
|
api.Tmpfs = tmpfs.NewTmpfs()
|
||||||
|
|
||||||
// mount api
|
// mount api
|
||||||
apiMux.HandleFunc("/hello", api.HandleOK)
|
apiMux.HandleFunc("/hello", api.HandleOK)
|
||||||
@@ -585,6 +709,8 @@ func NewAPI(apiConfig APIConfig) (*API, error) {
|
|||||||
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
apiMux.HandleFunc("/get_ffmpeg_config_list", api.HandleGetFfmpegConfigs)
|
||||||
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
apiMux.HandleFunc("/feedback", api.HandleFeedback)
|
||||||
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
apiMux.HandleFunc("/get_file_info", api.HandleGetFileInfo)
|
||||||
|
apiMux.HandleFunc("/get_file_stream_direct", api.HandleGetFileStreamDirect)
|
||||||
|
apiMux.HandleFunc("/prepare_file_stream_direct", api.HandlePrepareFileStreamDirect)
|
||||||
// below needs token
|
// below needs token
|
||||||
apiMux.HandleFunc("/walk", api.HandleWalk)
|
apiMux.HandleFunc("/walk", api.HandleWalk)
|
||||||
apiMux.HandleFunc("/reset", api.HandleReset)
|
apiMux.HandleFunc("/reset", api.HandleReset)
|
||||||
|
|||||||
62
internal/pkg/tmpfs/tmpfs.go
Normal file
62
internal/pkg/tmpfs/tmpfs.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package tmpfs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tmpfs struct {
|
||||||
|
record map[string]int64
|
||||||
|
Root string
|
||||||
|
FileLifeTime int64
|
||||||
|
CleanerInternal int64
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) GetObjFilePath(id int64, configName string) (string) {
|
||||||
|
return filepath.Join(tmpfs.Root, strconv.FormatInt(id, 10) + "." + configName + ".ogg")
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTmpfs() *Tmpfs {
|
||||||
|
tmpfs := &Tmpfs{
|
||||||
|
record: make(map[string]int64),
|
||||||
|
FileLifeTime: 10*60, // ! important
|
||||||
|
CleanerInternal: 1,
|
||||||
|
Root: "/tmp/",
|
||||||
|
}
|
||||||
|
tmpfs.wg.Add(1)
|
||||||
|
go tmpfs.Cleaner()
|
||||||
|
return tmpfs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) Record(filename string) {
|
||||||
|
tmpfs.record[filename] = time.Now().Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) Exits(filename string) (bool) {
|
||||||
|
_, ok := tmpfs.record[filename]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tmpfs *Tmpfs) Cleaner() {
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
for key, value := range tmpfs.record {
|
||||||
|
if now - value > tmpfs.FileLifeTime {
|
||||||
|
err = os.Remove(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("[tmpfs] Failed to remove file", err)
|
||||||
|
}
|
||||||
|
log.Println("[tmpfs] Deleted file", key)
|
||||||
|
delete(tmpfs.record, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
12
internal/pkg/tmpfs/tmpfs_test.go
Normal file
12
internal/pkg/tmpfs/tmpfs_test.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package tmpfs
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTmpfs(t *testing.T) {
|
||||||
|
t.Log("Starting ...")
|
||||||
|
tmpfs := NewTmpfs()
|
||||||
|
tmpfs.FileLifeTime = 1
|
||||||
|
tmpfs.Record("/tmp/testfile")
|
||||||
|
t.Log(tmpfs.record)
|
||||||
|
tmpfs.wg.Wait()
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<router-view :token="token" @set_token="set_token" @play_audio="play_audio"></router-view>
|
<router-view :token="token" @set_token="set_token" @play_audio="play_audio"></router-view>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<component-audio-player @stop="stop" :file=playing_audio_file></component-audio-player>
|
<component-audio-player @stop="stop" @play_audio="play_audio" :file=playing_audio_file></component-audio-player>
|
||||||
<p>{{ token }}</p>
|
<p>{{ token }}</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
web/index.js
84
web/index.js
@@ -334,7 +334,11 @@ const component_file_dialog = {
|
|||||||
template: `
|
template: `
|
||||||
<dialog open v-if="show_dialog">
|
<dialog open v-if="show_dialog">
|
||||||
<p>{{ file.filename }}</p>
|
<p>{{ file.filename }}</p>
|
||||||
<p>Download 使用 Axios 异步下载<br />Play 调用网页播放器播放源文件<br />Stream 将串流播放稍低码率的文件</p>
|
<p>
|
||||||
|
Download 使用 Axios 异步下载<br />
|
||||||
|
Play 调用网页播放器播放源文件<br />
|
||||||
|
Stream 将串流播放稍低码率的文件<br />
|
||||||
|
</p>
|
||||||
<button @click="download_file(file)" :disabled="disabled">{{ computed_download_status }}</button>
|
<button @click="download_file(file)" :disabled="disabled">{{ computed_download_status }}</button>
|
||||||
<button @click="emit_play_audio">Play</button>
|
<button @click="emit_play_audio">Play</button>
|
||||||
<button @click="emit_stream_audio">Stream</button>
|
<button @click="emit_stream_audio">Stream</button>
|
||||||
@@ -477,12 +481,16 @@ const component_file = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const component_audio_player = {
|
const component_audio_player = {
|
||||||
emits: ['stop'],
|
emits: ['stop', 'play_audio'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loop: true,
|
loop: true,
|
||||||
ffmpeg_config: {},
|
ffmpeg_config: {},
|
||||||
show_dialog: false,
|
show_dialog: false,
|
||||||
|
is_preparing: false,
|
||||||
|
prepare: false,
|
||||||
|
prepared_filesize: 0,
|
||||||
|
playing_file: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: ["file"],
|
props: ["file"],
|
||||||
@@ -504,8 +512,10 @@ const component_audio_player = {
|
|||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<input type="checkbox" v-model="loop" />
|
<input type="checkbox" v-model="loop" />
|
||||||
<label>Loop</label><br />
|
<label>Loop</label>
|
||||||
<video v-if="computed_show" class="audio-player" :src="computed_playing_audio_file_url" controls autoplay :loop="loop">
|
<input type="checkbox" v-model="prepare" />
|
||||||
|
<label>Prepare</label><br />
|
||||||
|
<video v-if="computed_video_show" class="audio-player" :src="computed_playing_audio_file_url" controls autoplay :loop="loop">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
<component-stream-config @set_ffmpeg_config="set_ffmpeg_config"></component-stream-config>
|
<component-stream-config @set_ffmpeg_config="set_ffmpeg_config"></component-stream-config>
|
||||||
@@ -525,37 +535,79 @@ const component_audio_player = {
|
|||||||
this.$router.push({
|
this.$router.push({
|
||||||
path: '/search_folders',
|
path: '/search_folders',
|
||||||
query: {
|
query: {
|
||||||
folder_id: this.file.folder_id,
|
folder_id: this.this_file.folder_id,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
set_ffmpeg_config(ffmpeg_config) {
|
set_ffmpeg_config(ffmpeg_config) {
|
||||||
this.ffmpeg_config = ffmpeg_config
|
this.ffmpeg_config = ffmpeg_config
|
||||||
},
|
},
|
||||||
|
prepare_func() {
|
||||||
|
this.playing_file = {}
|
||||||
|
this.is_preparing = true
|
||||||
|
axios.post('/api/v1/prepare_file_stream_direct', {
|
||||||
|
id: this.file.id,
|
||||||
|
config_name: this.ffmpeg_config.name,
|
||||||
|
}).then(response => {
|
||||||
|
console.log(response.data)
|
||||||
|
this.prepared_filesize = response.data.filesize
|
||||||
|
this.is_preparing = false
|
||||||
|
var file = this.file
|
||||||
|
file.play_back_type = 'cached_stream'
|
||||||
|
file.filesize = response.data.filesize
|
||||||
|
this.playing_file = file
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
file() {
|
||||||
|
// 如果没有勾选 prepare 则直接播放
|
||||||
|
// 否则进入 prepare 流程
|
||||||
|
if (this.prepare) {
|
||||||
|
this.prepare_func()
|
||||||
|
} else {
|
||||||
|
this.playing_file = this.file
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ffmpeg_config() {
|
||||||
|
if (this.prepare) {
|
||||||
|
this.playing_file = {}
|
||||||
|
this.prepare_func()
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
computed_readable_size() {
|
computed_readable_size() {
|
||||||
let filesize = this.file.filesize
|
if (this.is_preparing) {
|
||||||
if (filesize < 1024) {
|
return 'Preparing...'
|
||||||
|
}
|
||||||
|
let filesize = this.playing_file.filesize
|
||||||
|
if (filesize < 1024 * 1024) {
|
||||||
return filesize
|
return filesize
|
||||||
}
|
}
|
||||||
if (filesize < 1024 * 1024) {
|
if (filesize < 1024 * 1024 * 1024) {
|
||||||
return Math.round(filesize / 1024) + 'K'
|
return Math.round(filesize / 1024) + 'K'
|
||||||
}
|
}
|
||||||
if (filesize < 1024 * 1024 * 1024) {
|
|
||||||
return Math.round(filesize / 1024 / 1024) + 'M'
|
|
||||||
}
|
|
||||||
if (filesize < 1024 * 1024 * 1024 * 1024) {
|
if (filesize < 1024 * 1024 * 1024 * 1024) {
|
||||||
return Math.round(filesize / 1024 / 1024 / 1024) + 'G'
|
return Math.round(filesize / 1024 / 1024) + 'M'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed_playing_audio_file_url() {
|
computed_playing_audio_file_url() {
|
||||||
if (this.file.play_back_type === 'raw') {
|
if (this.playing_file.play_back_type === 'raw') {
|
||||||
return '/api/v1/get_file_direct?id=' + this.file.id
|
return '/api/v1/get_file_direct?id=' + this.playing_file.id
|
||||||
} else if (this.file.play_back_type === 'stream') {
|
} else if (this.playing_file.play_back_type === 'stream') {
|
||||||
return '/api/v1/get_file_stream?id=' + this.file.id + '&config=' + this.ffmpeg_config.name
|
if (this.prepare) {
|
||||||
|
this.prepare_func()
|
||||||
|
} else {
|
||||||
|
return '/api/v1/get_file_stream?id=' + this.playing_file.id + '&config=' + this.ffmpeg_config.name
|
||||||
|
}
|
||||||
|
} else if (this.playing_file.play_back_type === 'cached_stream') {
|
||||||
|
return '/api/v1/get_file_stream_direct?id=' + this.playing_file.id + '&config=' + this.ffmpeg_config.name
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed_video_show() {
|
||||||
|
return this.playing_file.id ? true : false
|
||||||
|
},
|
||||||
computed_show() {
|
computed_show() {
|
||||||
return this.file.id ? true : false
|
return this.file.id ? true : false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user