package handlers import ( "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "github.com/gorilla/mux" "github.com/zhang2092/mediahls/internal/db" "github.com/zhang2092/mediahls/internal/pkg/convert" "github.com/zhang2092/mediahls/internal/pkg/fileutil" "github.com/zhang2092/mediahls/internal/pkg/logger" ) // obj // videoEditPageData 视频编辑数据 type videoEditPageData struct { Summary string ID string IDMsg string Title string TitleMsg string Images string ImagesMsg string Description string DescriptionMsg string OriginLink string OriginLinkMsg string Status int StatusMsg string } // videoDeleteRequest 视频删除请求参数 type videoDeleteRequest struct { ID string `json:"id"` } // view // videoView 视频播放页面 func (server *Server) videoView(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) xid := vars["xid"] result, _ := server.store.GetVideo(r.Context(), xid) server.renderLayout(w, r, result, "video/play.html.tmpl") } // videosView 视频列表页面 func (server *Server) videosView(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) page, err := strconv.Atoi(vars["page"]) if err != nil { page = 1 } ctx := r.Context() u := withUser(ctx) var result []db.Video videos, err := server.store.ListVideosByUser(ctx, db.ListVideosByUserParams{ UserID: u.ID, Limit: 16, Offset: int32((page - 1) * 16), }) if err == nil { for _, item := range videos { if len(item.Description) > 65 { temp := strings.TrimSpace(item.Description[0:65]) + "..." item.Description = temp } result = append(result, item) } } server.renderLayout(w, r, result, "video/videos.html.tmpl") } // editVideoView 视频编辑页面 func (server *Server) editVideoView(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) xid := vars["xid"] ctx := r.Context() u := withUser(ctx) vm := videoEditPageData{} if len(xid) > 0 { if v, err := server.store.GetVideo(ctx, xid); err == nil { if u.ID == v.UserID { vm.ID = v.ID vm.Title = v.Title vm.Images = v.Images vm.Description = v.Description vm.OriginLink = v.OriginLink vm.Status = int(v.Status) } } } server.renderEditVideo(w, r, vm) } // data // stream 视频HLS播放处理 func (server *Server) stream(response http.ResponseWriter, request *http.Request) { vars := mux.Vars(request) mId := vars["xid"] segName, ok := vars["segName"] if !ok { mediaBase := getMediaBase(mId) m3u8Name := "index.m3u8" serveHlsM3u8(response, request, mediaBase, m3u8Name) } else { mediaBase := getMediaBase(mId) serveHlsTs(response, request, mediaBase, segName) } } // editVideo 视频编辑 func (server *Server) editVideo(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if err := r.ParseForm(); err != nil { server.renderEditVideo(w, r, videoEditPageData{Summary: "请求网络错误, 请刷新重试"}) return } vm, ok := viladatorEditVedio(r) if !ok { server.renderEditVideo(w, r, vm) return } curTime := time.Now() ctx := r.Context() u := withUser(ctx) if len(vm.ID) == 0 { _, err := server.store.CreateVideo(ctx, db.CreateVideoParams{ ID: genId(), Title: vm.Title, Description: vm.Description, Images: vm.Images, OriginLink: vm.OriginLink, PlayLink: "", UserID: u.ID, CreateBy: u.Name, }) if err != nil { vm.Summary = "添加视频失败" server.renderEditVideo(w, r, vm) return } } else { v, err := server.store.GetVideo(ctx, vm.ID) if err != nil { vm.Summary = "视频数据错误" server.renderEditVideo(w, r, vm) return } var sta int32 = int32(vm.Status) if sta != -1 && sta != 200 { sta = v.Status } _, err = server.store.UpdateVideo(ctx, db.UpdateVideoParams{ ID: vm.ID, Title: vm.Title, Description: vm.Description, Images: vm.Images, Status: sta, UpdateAt: curTime, UpdateBy: u.Name, }) if err != nil { vm.Summary = "更新视频失败" server.renderEditVideo(w, r, vm) return } } http.Redirect(w, r, "/me/videos", http.StatusFound) } // deleteVideo 视频删除 func (server *Server) deleteVideo(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() var req videoDeleteRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { RespondErr(w, "参数错误", nil) return } err := server.store.DeleteVideo(r.Context(), req.ID) if err != nil { RespondErr(w, "删除失败", nil) return } Respond(w, "删除成功", nil, http.StatusOK) } // transfer 视频转码 func (server *Server) transfer(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) xid := vars["xid"] ctx := r.Context() v, err := server.store.GetVideo(ctx, xid) if err != nil { http.Error(w, "视频信息错误", http.StatusInternalServerError) return } u := withUser(ctx) v, err = server.store.UpdateVideoStatus(ctx, db.UpdateVideoStatusParams{ ID: v.ID, Status: 1, UpdateAt: time.Now(), UpdateBy: u.Name, }) if err != nil { http.Error(w, "视频转码错误", http.StatusInternalServerError) return } go func(v db.Video, name string) { ctx := context.Background() err := convert.ConvertHLS("media/"+v.ID+"/", strings.TrimPrefix(v.OriginLink, "/")) if err != nil { logger.Logger.Errorf("Convert HLS [%s]-[%s]: %v", v.ID, v.OriginLink, err) _, _ = server.store.UpdateVideoStatus(ctx, db.UpdateVideoStatusParams{ ID: v.ID, Status: 2, UpdateAt: time.Now(), UpdateBy: name, }) return } // 转码成功 if _, err = server.store.SetVideoPlay(ctx, db.SetVideoPlayParams{ ID: v.ID, Status: 200, PlayLink: "/media/" + v.ID + "/stream/", UpdateAt: time.Now(), UpdateBy: name, }); err != nil { logger.Logger.Errorf("Set Video Play [%s]-[%s]: %v", v.ID, v.OriginLink, err) return } logger.Logger.Infof("[%s]-[%s] 转码完成", v.ID, v.OriginLink) }(v, u.Name) w.WriteHeader(http.StatusOK) w.Write([]byte("视频正在转码中, 请稍后刷新页面")) } // method // renderEditVideo 渲染视频编辑页面 func (server *Server) renderEditVideo(w http.ResponseWriter, r *http.Request, data any) { server.renderLayout(w, r, data, "video/edit.html.tmpl") } // viladatorEditVedio 检验视频编辑数据 func viladatorEditVedio(r *http.Request) (videoEditPageData, bool) { ok := true status, _ := strconv.Atoi(r.PostFormValue("status")) resp := videoEditPageData{ ID: r.PostFormValue("id"), Title: r.PostFormValue("title"), Images: r.PostFormValue("images"), Description: r.PostFormValue("description"), OriginLink: r.PostFormValue("origin_link"), Status: status, } if len(resp.Title) == 0 { resp.TitleMsg = "请填写正确的标题" ok = false } exist, _ := fileutil.PathExists(strings.TrimPrefix(resp.Images, "/")) if !exist { resp.ImagesMsg = "请先上传图片" ok = false } if len(resp.Description) == 0 { resp.DescriptionMsg = "请填写描述" ok = false } exist, _ = fileutil.PathExists(strings.TrimPrefix(resp.OriginLink, "/")) if !exist { resp.OriginLinkMsg = "请先上传视频" ok = false } return resp, ok } // getMediaBase 获取视频m3u8文件路径 func getMediaBase(mId string) string { mediaRoot := "media" return fmt.Sprintf("%s/%s", mediaRoot, mId) } // serveHlsM3u8 返回m3u8文件 func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) { mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name) http.ServeFile(w, r, mediaFile) w.Header().Set("Content-Type", "application/x-mpegURL") } // serveHlsTs 返回ts文件 func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) { mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName) http.ServeFile(w, r, mediaFile) w.Header().Set("Content-Type", "video/MP2T") } /* // 直接播放mp4 video, err := os.Open("web/statics/git.mp4") if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("failed to open file")) return } defer video.Close() http.ServeContent(w, r, "git", time.Now(), video) */