diff --git a/internal/pkg/api/api.go b/internal/pkg/api/api.go index 9fc7fa1..5dc299c 100644 --- a/internal/pkg/api/api.go +++ b/internal/pkg/api/api.go @@ -7,6 +7,7 @@ import ( "io" "log" "msw-open-music/internal/pkg/database" + "msw-open-music/internal/pkg/tmpfs" "net/http" "os" "os/exec" @@ -20,6 +21,7 @@ type API struct { Server http.Server token string APIConfig APIConfig + Tmpfs *tmpfs.Tmpfs } 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() ids := q["id"] if len(ids) == 0 { - api.HandleErrorString(w, r, `parameter "id" can't be empty`) - return + err = errors.New(`parameter "id" can't be empty`) + api.HandleError(w, r, err) + return err } - id, err := strconv.Atoi(ids[0]) + _, err = strconv.Atoi(ids[0]) if err != nil { - api.HandleErrorString(w, r, `parameter "id" should be an integer`) - return + err = errors.New(`parameter "id" should be an integer`) + api.HandleError(w, r, err) + return err } configs := q["config"] 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 } + q := r.URL.Query() + ids := q["id"] + id, err := strconv.Atoi(ids[0]) + configs := q["config"] configName := configs[0] file, err := api.Db.GetFile(int64(id)) if err != nil { @@ -384,7 +402,7 @@ func (api *API) HandleGetFileStream(w http.ResponseWriter, r *http.Request) { } args := strings.Split(ffmpegConfig.Args, " ") startArgs := []string {"-i", path} - endArgs := []string {"-vn", "-f", "matroska", "-"} + endArgs := []string {"-vn", "-f", "ogg", "-"} ffmpegArgs := append(startArgs, args...) ffmpegArgs = append(ffmpegArgs, endArgs...) 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) { q := r.URL.Query() ids := q["id"] @@ -572,6 +695,7 @@ func NewAPI(apiConfig APIConfig) (*API, error) { }, APIConfig: apiConfig, } + api.Tmpfs = tmpfs.NewTmpfs() // mount api 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("/feedback", api.HandleFeedback) 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 apiMux.HandleFunc("/walk", api.HandleWalk) apiMux.HandleFunc("/reset", api.HandleReset) diff --git a/internal/pkg/tmpfs/tmpfs.go b/internal/pkg/tmpfs/tmpfs.go new file mode 100644 index 0000000..18d523e --- /dev/null +++ b/internal/pkg/tmpfs/tmpfs.go @@ -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) + } +} diff --git a/internal/pkg/tmpfs/tmpfs_test.go b/internal/pkg/tmpfs/tmpfs_test.go new file mode 100644 index 0000000..91a020d --- /dev/null +++ b/internal/pkg/tmpfs/tmpfs_test.go @@ -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() +} diff --git a/web/index.html b/web/index.html index d3d7310..1fc1214 100644 --- a/web/index.html +++ b/web/index.html @@ -29,7 +29,7 @@ diff --git a/web/index.js b/web/index.js index 7afe616..f4db5d4 100644 --- a/web/index.js +++ b/web/index.js @@ -334,7 +334,11 @@ const component_file_dialog = { template: ` {{ file.filename }} - Download 使用 Axios 异步下载Play 调用网页播放器播放源文件Stream 将串流播放稍低码率的文件 + + Download 使用 Axios 异步下载 + Play 调用网页播放器播放源文件 + Stream 将串流播放稍低码率的文件 + {{ computed_download_status }} Play Stream @@ -477,12 +481,16 @@ const component_file = { } const component_audio_player = { - emits: ['stop'], + emits: ['stop', 'play_audio'], data() { return { loop: true, ffmpeg_config: {}, show_dialog: false, + is_preparing: false, + prepare: false, + prepared_filesize: 0, + playing_file: {}, } }, props: ["file"], @@ -504,8 +512,10 @@ const component_audio_player = { -Loop - +Loop + +Prepare + @@ -525,37 +535,79 @@ const component_audio_player = { this.$router.push({ path: '/search_folders', query: { - folder_id: this.file.folder_id, + folder_id: this.this_file.folder_id, } }) }, set_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_readable_size() { - let filesize = this.file.filesize - if (filesize < 1024) { + if (this.is_preparing) { + return 'Preparing...' + } + let filesize = this.playing_file.filesize + if (filesize < 1024 * 1024) { return filesize } - if (filesize < 1024 * 1024) { + if (filesize < 1024 * 1024 * 1024) { return Math.round(filesize / 1024) + 'K' } - if (filesize < 1024 * 1024 * 1024) { - return Math.round(filesize / 1024 / 1024) + 'M' - } 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() { - if (this.file.play_back_type === 'raw') { - return '/api/v1/get_file_direct?id=' + this.file.id - } else if (this.file.play_back_type === 'stream') { - return '/api/v1/get_file_stream?id=' + this.file.id + '&config=' + this.ffmpeg_config.name + if (this.playing_file.play_back_type === 'raw') { + return '/api/v1/get_file_direct?id=' + this.playing_file.id + } else if (this.playing_file.play_back_type === 'stream') { + 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() { return this.file.id ? true : false },
{{ file.filename }}
Download 使用 Axios 异步下载Play 调用网页播放器播放源文件Stream 将串流播放稍低码率的文件
+ Download 使用 Axios 异步下载 + Play 调用网页播放器播放源文件 + Stream 将串流播放稍低码率的文件 +