This commit is contained in:
kenneth 2023-12-01 02:46:50 +00:00
parent 90e6ba5070
commit 1bb57bc94a
26 changed files with 1312 additions and 169 deletions

View File

@ -15,3 +15,19 @@ type User struct {
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
type Video struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Images string `json:"images"`
OriginLink string `json:"origin_link"`
PlayLink string `json:"play_link"`
Status int32 `json:"status"`
IsDeleted bool `json:"is_deleted"`
UserID string `json:"user_id"`
CreateAt time.Time `json:"create_at"`
CreateBy string `json:"create_by"`
UpdateAt time.Time `json:"update_at"`
UpdateBy string `json:"update_by"`
}

View File

@ -10,12 +10,20 @@ import (
type Querier interface {
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
CreateVideo(ctx context.Context, arg CreateVideoParams) (Video, error)
DeleteUser(ctx context.Context, id string) error
DeleteVideo(ctx context.Context, id string) error
GetUser(ctx context.Context, id string) (User, error)
GetUserByEmail(ctx context.Context, email string) (User, error)
GetUserByName(ctx context.Context, username string) (User, error)
GetVideo(ctx context.Context, id string) (Video, error)
ListUsers(ctx context.Context, arg ListUsersParams) ([]User, error)
ListVideos(ctx context.Context, arg ListVideosParams) ([]Video, error)
ListVideosByUser(ctx context.Context, arg ListVideosByUserParams) ([]Video, error)
SetVideoPlay(ctx context.Context, arg SetVideoPlayParams) (Video, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
UpdateVideo(ctx context.Context, arg UpdateVideoParams) (Video, error)
UpdateVideoStatus(ctx context.Context, arg UpdateVideoStatusParams) (Video, error)
}
var _ Querier = (*Queries)(nil)

View File

@ -0,0 +1,58 @@
-- name: CreateVideo :one
INSERT INTO videos (
id, title, description, images, origin_link, play_link, user_id, create_by
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
RETURNING *;
-- name: DeleteVideo :exec
UPDATE videos
SET is_deleted = TRUE
WHERE id = $1;
-- name: UpdateVideoStatus :one
UPDATE videos
SET status = $2,
update_at = $3,
update_by = $4
WHERE id = $1
RETURNING *;
-- name: SetVideoPlay :one
UPDATE videos
SET status = $2,
play_link = $3,
update_at = $4,
update_by = $5
WHERE id = $1
RETURNING *;
-- name: UpdateVideo :one
UPDATE videos
SET title = $2,
description = $3,
images = $4,
status = $5,
update_at = $6,
update_by = $7
WHERE id = $1
RETURNING *;
-- name: GetVideo :one
SELECT * FROM videos
WHERE id = $1 LIMIT 1;
-- name: ListVideos :many
SELECT * FROM videos
WHERE is_deleted = FALSE AND status=200
ORDER BY id DESC
LIMIT $1
OFFSET $2;
-- name: ListVideosByUser :many
SELECT * FROM videos
WHERE is_deleted = FALSE AND user_id = $1
ORDER BY id DESC
LIMIT $2
OFFSET $3;

View File

@ -1 +1,2 @@
DROP TABLE "videos";
DROP TABLE "users";

View File

@ -1,7 +1,39 @@
CREATE TABLE "users" (
"id" varchar NOT NULL PRIMARY KEY,
"username" varchar NOT NULL UNIQUE,
"username" varchar NOT NULL,
"hashed_password" varchar NOT NULL,
"email" varchar NOT NULL UNIQUE,
"email" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
);
ALTER TABLE "users" ADD CONSTRAINT "username_key" UNIQUE ("username");
ALTER TABLE "users" ADD CONSTRAINT "email_key" UNIQUE ("email");
CREATE INDEX ON "users" ("username");
CREATE INDEX ON "users" ("email");
CREATE TABLE "videos" (
"id" varchar NOT NULL PRIMARY KEY,
"title" varchar NOT NULL,
"description" varchar NOT NULL,
"images" varchar NOT NULL,
"origin_link" varchar NOT NULL,
"play_link" varchar NOT NULL,
-- 100: 下架
-- 0: 添加视频
-- 1: 视频转码中
-- 2: 视频转码失败
-- 200: 视频正常显示播放
"status" int NOT NULL DEFAULT (0),
"is_deleted" boolean NOT NULL DEFAULT false, -- 删除
"user_id" varchar NOT NULL,
"create_at" timestamptz NOT NULL DEFAULT (now()),
"create_by" varchar NOT NULL,
"update_at" timestamptz NOT NULL DEFAULT('0001-01-01 00:00:00Z'),
"update_by" varchar NOT NULL DEFAULT ('')
);
ALTER TABLE "videos" ADD FOREIGN KEY ("user_id") REFERENCES "users" ("id");
CREATE INDEX ON "videos" ("status");
CREATE INDEX ON "videos" ("user_id", "status", "create_at");
CREATE INDEX ON "videos" ("title");
CREATE INDEX ON "videos" ("user_id", "title");

337
internal/db/video.sql.go Normal file
View File

@ -0,0 +1,337 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.24.0
// source: video.sql
package db
import (
"context"
"time"
)
const createVideo = `-- name: CreateVideo :one
INSERT INTO videos (
id, title, description, images, origin_link, play_link, user_id, create_by
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
)
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
`
type CreateVideoParams struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Images string `json:"images"`
OriginLink string `json:"origin_link"`
PlayLink string `json:"play_link"`
UserID string `json:"user_id"`
CreateBy string `json:"create_by"`
}
func (q *Queries) CreateVideo(ctx context.Context, arg CreateVideoParams) (Video, error) {
row := q.db.QueryRowContext(ctx, createVideo,
arg.ID,
arg.Title,
arg.Description,
arg.Images,
arg.OriginLink,
arg.PlayLink,
arg.UserID,
arg.CreateBy,
)
var i Video
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
)
return i, err
}
const deleteVideo = `-- name: DeleteVideo :exec
UPDATE videos
SET is_deleted = TRUE
WHERE id = $1
`
func (q *Queries) DeleteVideo(ctx context.Context, id string) error {
_, err := q.db.ExecContext(ctx, deleteVideo, id)
return err
}
const getVideo = `-- name: GetVideo :one
SELECT id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by FROM videos
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetVideo(ctx context.Context, id string) (Video, error) {
row := q.db.QueryRowContext(ctx, getVideo, id)
var i Video
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
)
return i, err
}
const listVideos = `-- name: ListVideos :many
SELECT id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by FROM videos
WHERE is_deleted = FALSE AND status=200
ORDER BY id DESC
LIMIT $1
OFFSET $2
`
type ListVideosParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListVideos(ctx context.Context, arg ListVideosParams) ([]Video, error) {
rows, err := q.db.QueryContext(ctx, listVideos, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Video{}
for rows.Next() {
var i Video
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listVideosByUser = `-- name: ListVideosByUser :many
SELECT id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by FROM videos
WHERE is_deleted = FALSE AND user_id = $1
ORDER BY id DESC
LIMIT $2
OFFSET $3
`
type ListVideosByUserParams struct {
UserID string `json:"user_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
func (q *Queries) ListVideosByUser(ctx context.Context, arg ListVideosByUserParams) ([]Video, error) {
rows, err := q.db.QueryContext(ctx, listVideosByUser, arg.UserID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Video{}
for rows.Next() {
var i Video
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const setVideoPlay = `-- name: SetVideoPlay :one
UPDATE videos
SET status = $2,
play_link = $3,
update_at = $4,
update_by = $5
WHERE id = $1
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
`
type SetVideoPlayParams struct {
ID string `json:"id"`
Status int32 `json:"status"`
PlayLink string `json:"play_link"`
UpdateAt time.Time `json:"update_at"`
UpdateBy string `json:"update_by"`
}
func (q *Queries) SetVideoPlay(ctx context.Context, arg SetVideoPlayParams) (Video, error) {
row := q.db.QueryRowContext(ctx, setVideoPlay,
arg.ID,
arg.Status,
arg.PlayLink,
arg.UpdateAt,
arg.UpdateBy,
)
var i Video
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
)
return i, err
}
const updateVideo = `-- name: UpdateVideo :one
UPDATE videos
SET title = $2,
description = $3,
images = $4,
status = $5,
update_at = $6,
update_by = $7
WHERE id = $1
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
`
type UpdateVideoParams struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Images string `json:"images"`
Status int32 `json:"status"`
UpdateAt time.Time `json:"update_at"`
UpdateBy string `json:"update_by"`
}
func (q *Queries) UpdateVideo(ctx context.Context, arg UpdateVideoParams) (Video, error) {
row := q.db.QueryRowContext(ctx, updateVideo,
arg.ID,
arg.Title,
arg.Description,
arg.Images,
arg.Status,
arg.UpdateAt,
arg.UpdateBy,
)
var i Video
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
)
return i, err
}
const updateVideoStatus = `-- name: UpdateVideoStatus :one
UPDATE videos
SET status = $2,
update_at = $3,
update_by = $4
WHERE id = $1
RETURNING id, title, description, images, origin_link, play_link, status, is_deleted, user_id, create_at, create_by, update_at, update_by
`
type UpdateVideoStatusParams struct {
ID string `json:"id"`
Status int32 `json:"status"`
UpdateAt time.Time `json:"update_at"`
UpdateBy string `json:"update_by"`
}
func (q *Queries) UpdateVideoStatus(ctx context.Context, arg UpdateVideoStatusParams) (Video, error) {
row := q.db.QueryRowContext(ctx, updateVideoStatus,
arg.ID,
arg.Status,
arg.UpdateAt,
arg.UpdateBy,
)
var i Video
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Images,
&i.OriginLink,
&i.PlayLink,
&i.Status,
&i.IsDeleted,
&i.UserID,
&i.CreateAt,
&i.CreateBy,
&i.UpdateAt,
&i.UpdateBy,
)
return i, err
}

View File

@ -14,9 +14,9 @@ const (
type CtxTypeUser string
type authorize struct {
AuthID string `json:"auth_id"`
AuthName string `json:"auth_name"`
type Authorize struct {
ID string `json:"id"`
Name string `json:"name"`
}
func genId() string {

View File

@ -1,20 +1,39 @@
package handlers
import (
"log"
"net/http"
"strings"
"github.com/zhang2092/mediahls/internal/db"
)
type pageData struct {
AuthID string
AuthName string
Authorize
Videos []db.Video
}
func (server *Server) home(w http.ResponseWriter, r *http.Request) {
pd := pageData{}
auth, err := server.withCookie(r)
if err == nil {
pd.AuthID = auth.AuthID
pd.AuthName = auth.AuthName
pd.Authorize = *auth
}
ctx := r.Context()
videos, err := server.store.ListVideos(ctx, db.ListVideosParams{
Limit: 100,
Offset: 0,
})
if err == nil {
for _, item := range videos {
if len(item.Description) > 65 {
temp := strings.TrimSpace(item.Description[0:65]) + "..."
item.Description = temp
log.Println(item.Description)
}
pd.Videos = append(pd.Videos, item)
}
}
renderHome(w, pd)
}

View File

@ -30,14 +30,14 @@ func (server *Server) authorizeMiddleware(next http.Handler) http.Handler {
})
}
func (server *Server) withCookie(r *http.Request) (*authorize, error) {
func (server *Server) withCookie(r *http.Request) (*Authorize, error) {
cookie, err := r.Cookie(AuthorizeCookie)
if err != nil {
log.Printf("get cookie: %v", err)
return nil, err
}
u := &authorize{}
u := &Authorize{}
err = server.secureCookie.Decode(AuthorizeCookie, cookie.Value, u)
if err != nil {
log.Printf("secure decode cookie: %v", err)
@ -47,19 +47,13 @@ func (server *Server) withCookie(r *http.Request) (*authorize, error) {
return u, nil
}
func withUser(ctx context.Context) *authorize {
var result authorize
func withUser(ctx context.Context) Authorize {
var result Authorize
ctxValue, err := convert.ToByteE(ctx.Value(ContextUser))
log.Printf("ctx: %s", ctxValue)
if err != nil {
log.Printf("1: %v", err)
return nil
}
err = json.Unmarshal(ctxValue, &result)
if err != nil {
log.Printf("2: %v", err)
return nil
return result
}
return &result
json.Unmarshal(ctxValue, &result)
return result
}

View File

@ -55,6 +55,7 @@ func (server *Server) setupRouter() {
router := mux.NewRouter()
router.Use(mux.CORSMethodMiddleware(router))
router.PathPrefix("/statics/").Handler(http.StripPrefix("/statics/", http.FileServer(http.Dir("web/statics"))))
router.PathPrefix("/upload/imgs").Handler(http.StripPrefix("/upload/imgs/", http.FileServer(http.Dir("upload/imgs"))))
router.HandleFunc("/register", server.registerView).Methods(http.MethodGet)
router.HandleFunc("/register", server.register).Methods(http.MethodPost)
@ -68,8 +69,17 @@ func (server *Server) setupRouter() {
subRouter := router.PathPrefix("/").Subrouter()
subRouter.Use(server.authorizeMiddleware)
subRouter.HandleFunc("/upload", server.uploadView).Methods(http.MethodGet)
subRouter.HandleFunc("/upload", server.upload).Methods(http.MethodPost)
subRouter.HandleFunc("/me/videos", server.videosView).Methods(http.MethodGet)
subRouter.HandleFunc("/me/videos/p{page}", server.videosView).Methods(http.MethodGet)
subRouter.HandleFunc("/me/videos/create", server.createVideoView).Methods(http.MethodGet)
subRouter.HandleFunc("/me/videos/create/{xid}", server.createVideoView).Methods(http.MethodGet)
subRouter.HandleFunc("/me/videos/create", server.createVideo).Methods(http.MethodPost)
subRouter.HandleFunc("/upload_image", server.uploadImage).Methods(http.MethodPost)
subRouter.HandleFunc("/upload_file", server.uploadVideo).Methods(http.MethodPost)
subRouter.HandleFunc("/transfer/{xid}", server.transferView).Methods(http.MethodGet)
subRouter.HandleFunc("/transfer/{xid}", server.transfer).Methods(http.MethodPost)
server.router = router
}

View File

@ -1,6 +1,8 @@
package handlers
import (
"bufio"
"errors"
"io"
"log"
"net/http"
@ -13,17 +15,7 @@ import (
"github.com/zhang2092/mediahls/internal/pkg/fileutil"
)
func (server *Server) uploadView(w http.ResponseWriter, r *http.Request) {
user := withUser(r.Context())
log.Printf("%v", user)
renderUpload(w, nil)
}
func renderUpload(w http.ResponseWriter, data any) {
render(w, data, "web/templates/me/upload.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
func (server *Server) uploadVideo(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
file, fileHeader, err := r.FormFile("file")
@ -56,7 +48,8 @@ func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
return
}
dir := path.Join("upload", time.Now().Format("20060102"))
curTime := time.Now()
dir := path.Join("upload", "files", curTime.Format("2006"), curTime.Format("01"), curTime.Format("02"))
exist, _ := fileutil.PathExists(dir)
if !exist {
err := os.MkdirAll(dir, os.ModePerm)
@ -87,6 +80,60 @@ func (server *Server) upload(w http.ResponseWriter, r *http.Request) {
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte("/" + filePath))
if err != nil {
log.Printf("%v", err)
}
}
func (server *Server) uploadImage(w http.ResponseWriter, r *http.Request) {
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(r.Body)
_, fh, err := r.FormFile("file")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
f, err := fh.Open()
if err != nil {
log.Printf("%v", err)
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte("读取图片失败"))
if err != nil {
log.Printf("%v", err)
}
return
}
reader := bufio.NewReader(f)
filePath, err := fileutil.UploadImage(reader)
if errors.Is(err, fileutil.ErrUnsupportedFileFormat) {
log.Printf("%v", err)
w.WriteHeader(http.StatusUnsupportedMediaType)
_, err = w.Write([]byte(fileutil.ErrUnsupportedFileFormat.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
if err != nil {
log.Printf("%v", err)
w.WriteHeader(http.StatusInternalServerError)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Printf("%v", err)
}
return
}
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(filePath))
if err != nil {

View File

@ -61,6 +61,7 @@ func viladatorRegister(email, username, password string) (*respErrs, bool) {
}
type respErrs struct {
Authorize
Summary string
Email string
Username string
@ -191,7 +192,7 @@ func (server *Server) login(w http.ResponseWriter, r *http.Request) {
return
}
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &authorize{AuthID: user.ID, AuthName: user.Username})
encoded, err := server.secureCookie.Encode(AuthorizeCookie, &Authorize{ID: user.ID, Name: user.Username})
if err != nil {
errs.Summary = "请求网络错误,请刷新重试(cookie)"
renderLogin(w, errs)

View File

@ -1,28 +1,36 @@
package handlers
import (
"context"
"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"
)
type playData struct {
AuthID string
AuthName string
Url string
Authorize
Url string
Video db.Video
}
func (server *Server) play(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
xid := vars["xid"]
video, _ := server.store.GetVideo(r.Context(), xid)
data := playData{
Url: "/media/" + xid + "/stream/",
Video: video,
}
auth, err := server.withCookie(r)
if err == nil {
data.AuthID = auth.AuthID
data.AuthName = auth.AuthName
data.Authorize = *auth
}
render(w, data, "web/templates/video/play.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
@ -43,10 +51,7 @@ http.ServeContent(w, r, "git", time.Now(), video)
func (server *Server) stream(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
mId := vars["xid"]
fmt.Println(mId)
segName, ok := vars["segName"]
if !ok {
mediaBase := getMediaBase(mId)
m3u8Name := "index.m3u8"
@ -55,29 +60,261 @@ func (server *Server) stream(response http.ResponseWriter, request *http.Request
mediaBase := getMediaBase(mId)
serveHlsTs(response, request, mediaBase, segName)
}
}
func getMediaBase(mId string) string {
mediaRoot := "media"
return fmt.Sprintf("%s/%s", mediaRoot, mId)
}
func serveHlsM3u8(w http.ResponseWriter, r *http.Request, mediaBase, m3u8Name string) {
fmt.Println("serveHlsM3u8...")
mediaFile := fmt.Sprintf("%s/%s", mediaBase, m3u8Name)
fmt.Println(mediaFile)
http.ServeFile(w, r, mediaFile)
w.Header().Set("Content-Type", "application/x-mpegURL")
}
func serveHlsTs(w http.ResponseWriter, r *http.Request, mediaBase, segName string) {
fmt.Println("serveHlsTs...")
mediaFile := fmt.Sprintf("%s/%s", mediaBase, segName)
fmt.Println(mediaFile)
http.ServeFile(w, r, mediaFile)
w.Header().Set("Content-Type", "video/MP2T")
}
type meVideoData struct {
Authorize
Videos []db.Video
}
func (server *Server) videosView(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
data := meVideoData{
Authorize: withUser(ctx),
}
vars := mux.Vars(r)
page, err := strconv.Atoi(vars["page"])
if err != nil {
page = 1
}
videos, err := server.store.ListVideosByUser(ctx, db.ListVideosByUserParams{
UserID: data.Authorize.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
}
data.Videos = append(data.Videos, item)
}
}
render(w, data, "web/templates/me/videos.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
func (server *Server) createVideoView(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
xid := vars["xid"]
vm := videoCreateResp{
Authorize: withUser(r.Context()),
}
if len(xid) > 0 {
if v, err := server.store.GetVideo(r.Context(), xid); err == nil {
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)
}
}
renderCreateVideo(w, vm)
}
func renderCreateVideo(w http.ResponseWriter, data any) {
render(w, data, "web/templates/video/edit.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
type videoCreateResp struct {
Authorize
Summary string
ID string
IDErr string
Title string
TitleErr string
Images string
ImagesErr string
Description string
DescriptionErr string
OriginLink string
OriginLinkErr string
Status int
StatusErr string
}
func viladatorCreateVedio(r *http.Request) (*videoCreateResp, bool) {
ok := true
status, _ := strconv.Atoi(r.PostFormValue("status"))
errs := &videoCreateResp{
Authorize: withUser(r.Context()),
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(errs.Title) == 0 {
errs.TitleErr = "请填写正确的标题"
ok = false
}
exist, _ := fileutil.PathExists(strings.TrimPrefix(errs.Images, "/"))
if !exist {
errs.ImagesErr = "请先上传图片"
ok = false
}
if len(errs.Description) == 0 {
errs.DescriptionErr = "请填写描述"
ok = false
}
exist, _ = fileutil.PathExists(strings.TrimPrefix(errs.OriginLink, "/"))
if !exist {
errs.OriginLinkErr = "请先上传视频"
ok = false
}
return errs, ok
}
func (server *Server) createVideo(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if err := r.ParseForm(); err != nil {
renderCreateVideo(w, videoCreateResp{Summary: "请求网络错误, 请刷新重试"})
return
}
vm, ok := viladatorCreateVedio(r)
if !ok {
renderCreateVideo(w, 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 = "添加视频失败"
renderCreateVideo(w, vm)
return
}
} else {
_, err := server.store.UpdateVideo(ctx, db.UpdateVideoParams{
ID: vm.ID,
Title: vm.Title,
Description: vm.Description,
Images: vm.Images,
Status: int32(vm.Status),
UpdateAt: curTime,
UpdateBy: u.Name,
})
if err != nil {
vm.Summary = "更新视频失败"
renderCreateVideo(w, vm)
return
}
}
http.Redirect(w, r, "/me/videos", http.StatusFound)
}
type transferData struct {
Authorize
Video db.Video
}
func (server *Server) transferView(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
xid := vars["xid"]
v, _ := server.store.GetVideo(r.Context(), xid)
data := transferData{
Video: v,
}
u, err := server.withCookie(r)
if err == nil {
data.Authorize = *u
}
render(w, data, "web/templates/video/transfer.html.tmpl", "web/templates/base/header.html.tmpl", "web/templates/base/footer.html.tmpl")
}
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("视频正在转码中, 请稍后刷新页面"))
}

View File

@ -1,6 +1,28 @@
package fileutil
import "os"
import (
"errors"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"os"
"path"
"time"
nanoId "github.com/matoous/go-nanoid"
)
var (
// 图片最大数量
MaxImageSize = 10 << 20
ErrUnsupportedFileFormat = errors.New("图片格式不支持")
ErrFileStorePath = errors.New("文件存储路径异常")
ErrFileGenerateName = errors.New("文件名称生成异常")
ErrFileSaveFailed = errors.New("文件保存失败")
)
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
@ -12,3 +34,60 @@ func PathExists(path string) (bool, error) {
}
return false, err
}
func Mkdir(path string) error {
return os.MkdirAll(path, os.ModePerm)
}
func UploadImage(r io.Reader) (string, error) {
img, format, err := image.Decode(r)
if err != nil {
return "", fmt.Errorf("读取图片错误")
}
if errors.Is(err, image.ErrFormat) {
return "", ErrUnsupportedFileFormat
}
if format != "png" && format != "jpeg" && format != "gif" {
return "", ErrUnsupportedFileFormat
}
// 存放目录
curTime := time.Now()
dir := path.Join("upload", "imgs", curTime.Format("2006"), curTime.Format("01"), curTime.Format("02"))
exist, _ := PathExists(dir)
if !exist {
err := Mkdir(dir)
if err != nil {
return "", ErrFileStorePath
}
}
filename, err := nanoId.Nanoid()
if err != nil {
return "", ErrFileGenerateName
}
filePath := path.Join("", dir, filename+"."+format)
f, err := os.Create(filePath)
if err != nil {
return "", ErrFileStorePath
}
defer func(f *os.File) {
_ = f.Close()
}(f)
var e error
switch format {
case "png":
e = png.Encode(f, img)
case "jpeg":
e = jpeg.Encode(f, img, nil)
case "gif":
e = gif.Encode(f, img, nil)
}
if e != nil {
return "", ErrFileSaveFailed
}
return "/" + filePath, nil
}

View File

@ -11,6 +11,10 @@ import (
)
func main() {
// filename, _ := nanoId.Nanoid()
// log.Println(filename)
// return
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("cannot load config: ", err)

View File

@ -1,5 +1,11 @@
@charset "utf-8";
html,
body {
/* 禁用空格键的滚动 */
scroll-behavior: auto;
}
.flex {
display: flex;
}
@ -22,7 +28,7 @@
.navbar-wh {
padding: 0.2rem 1.6rem;
border-bottom: 1px solid rgba(0,0,0,.12);
border-bottom: 1px solid rgba(0, 0, 0, .12);
}
.navbar-brand-fs {
@ -54,6 +60,17 @@
padding-bottom: 60px;
}
.main .title{
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 30px;
}
.main .title a{
height: 35px;
}
.tip-box {
width: 100%;
background-color: rgb(229, 246, 253);
@ -68,7 +85,7 @@
margin-right: 12px;
}
.tip-box .tip-icon svg{
.tip-box .tip-icon svg {
fill: currentColor;
color: #007bff;
}
@ -77,7 +94,18 @@
letter-spacing: 0.00938em;
font-size: 13px;
font-weight: 400;
color:rgb(1, 67, 97);
color: rgb(1, 67, 97);
margin-bottom: 0;
line-height: 24px;
}
.video-list {
display: grid;
/* 声明列的宽度 */
grid-template-columns: repeat(4, 305px);
/* 声明行间距和列间距 */
grid-gap: 20px;
/* 声明行的高度 */
/* grid-template-rows: 360px; */
margin-top: 25px;
}

2
web/statics/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@
<div class="wrapper"> -->
{{define "footer"}}
</div>
<script src="/statics/js/jquery.slim.min.js"></script>
<script src="/statics/js/jquery.min.js"></script>
<script src="/statics/js/bootstrap.bundle.min.js"></script>
{{block "js" .}}{{end}}
</body>

View File

@ -21,11 +21,14 @@
HLS流媒体
</a>
<ul class="flex oauth">
{{if .AuthID}}
<li>
欢迎您: {{.AuthName}}
{{if .Authorize.ID}}
<li style="font-size: 12px;">
欢迎您: {{.Authorize.Name}}
</li>
<li>
<li style="font-size: 12px;">
<a href="/me/videos">我的视频</a>
</li>
<li style="font-size: 12px;">
<a href="/logout" class="btn btn-primary btn-sm">退出</a>
</li>
{{else}}

View File

@ -1,51 +1,23 @@
<!-- <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>首页 - HLS流视频</title>
<style>
.container{
width: 640px;
}
.container #video{
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<video id="video" controls></video>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script>
var video = document.getElementById('video');
if(Hls.isSupported()) {
console.log("supported");
var hls = new Hls();
hls.loadSource('http://192.168.9.252:9090/statics/clip7iilsbb2u3en7mt0/index.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
console.log("no supported");
video.src = 'http://192.168.9.252:9090/stream';
video.addEventListener('loadedmetadata',function() {
video.play();
});
}
</script>
</body>
</html> -->
{{template "header" .}}
<div class="container-fluid flex justify-content">
<div class="main">
<h3 style="margin-top: 20px;margin-bottom: 10px;">视频列表</h3>
<div class="video-list">
{{range .Videos}}
<div class="card">
<img src="{{.Images}}" class="card-img-top" alt="{{.Title}}">
<div class="card-body">
<h5 class="card-title">{{.Title}}</h5>
<p class="card-text">{{.Description}}</p>
<a href="/play/{{.ID}}" class="btn btn-primary">播放</a>
</div>
</div>
{{end}}
</div>
</div>
</div>
{{define "js"}}
<script>
</script>
{{end}}
{{template "footer" .}}

View File

@ -0,0 +1,73 @@
{{template "header" .}}
<div class="container-fluid flex justify-content">
<div class="main">
<div class="title">
<h3>我的视频</h3>
<a href="/me/videos/create" class="btn btn-primary">添加</a>
</div>
<div class="video-list">
{{range .Videos}}
<div class="card">
<img src="{{.Images}}" class="card-img-top" alt="{{.Title}}">
<div class="card-body">
<h5 class="card-title">{{.Title}}</h5>
<p class="card-text">{{.Description}}</p>
<a href="/me/videos/create/{{.ID}}" class="btn btn-warning">编辑</a>
<button id="del" data-id="{{.ID}}" class="btn btn-danger">删除</button>
{{if eq .Status 0}}
<button id="transfer" data-id="{{.ID}}" class="btn btn-info">转码</button>
{{else if eq .Status 200}}
<a href="/play/{{.ID}}" class="btn btn-primary">播放</a>
{{end}}
<div>
{{if eq .Status 1}}
<p>转码中...</p>
{{else if eq .Status 2}}
<p>转码失败</p>
{{end}}
<p id="msg"></p>
</div>
</div>
</div>
{{end}}
</div>
</div>
</div>
{{define "js"}}
<script>
$('#transfer').click(function () {
let that = $(this)
that.attr("disable", true).html('转码中...')
let id = that.attr("data-id")
$.ajax({
url: '/transfer/' + id,
type: 'post',
success: function (obj) {
$('#msg').html(obj)
},
error: function (ex) {
console.log(ex)
}
});
});
$('#del').click(function () {
let that = $(this)
that.attr("disable", true).html('删除中...')
let id = that.attr("data-id")
$.ajax({
url: '/transfer/' + id,
type: 'post',
success: function (obj) {
$('#msg').html(obj)
},
error: function (ex) {
console.log(ex)
}
});
});
</script>
{{end}}
{{template "footer" .}}

View File

@ -1,31 +1,41 @@
{{template "header" .}}
<div class="container">
<div class="flex flex-column align-items row py-md-5">
<h1>登录</h1>
<div class="col-sm-5 py-md-5">
<form action="/login" method="post">
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}" aria-describedby="emailValid">
{{if .EmailErr}}
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
{{end}}
<div class="flex flex-column align-items row py-md-5 mt-md-5">
<h1>登录</h1>
<div class="col-sm-4 py-md-5">
<form action="/login" method="post">
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">邮箱</span>
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}" aria-describedby="passwordValid">
{{if .PasswordErr}}
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
{{end}}
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
{{if .Summary}}
<div class="py-md-5" style="color: #f44336;">
{{.Summary}}
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}"
aria-describedby="emailValid">
</div>
{{if .EmailErr}}
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
{{end}}
</div>
{{end}}
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">密码</span>
</div>
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}"
aria-describedby="passwordValid">
</div>
{{if .PasswordErr}}
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
{{end}}
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
{{if .Summary}}
<div class="py-md-5" style="color: #f44336;">
{{.Summary}}
</div>
{{end}}
</div>
</div>
</div>
{{template "footer" .}}

View File

@ -1,38 +1,53 @@
{{template "header" .}}
<div class="container">
<div class="flex flex-column align-items row py-md-5">
<h1>注册</h1>
<div class="col-sm-5 py-md-5">
<form action="/register" method="post">
<div class="form-group">
<label for="email">邮箱</label>
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}" aria-describedby="emailValid">
{{if .EmailErr}}
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
{{end}}
<div class="flex flex-column align-items row py-md-5 mt-md-5">
<h1>注册</h1>
<div class="col-sm-4 py-md-5">
<form action="/register" method="post">
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">邮箱</span>
</div>
<div class="form-group">
<label for="username">名称</label>
<input type="text" name="username" class="form-control" required id="username" value="{{.Username}}" style="width: 100%;" aria-describedby="usernameValid">
{{if .UsernameErr}}
<small id="usernameValid" style="color: #f44336;" class="form-text">{{.UsernameErr}}</small>
{{end}}
</div>
<div class="form-group">
<label for="password">密码</label>
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}" aria-describedby="passwordValid">
{{if .PasswordErr}}
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
{{end}}
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
{{if .Summary}}
<div class="py-md-5" style="color: #f44336;">
{{.Summary}}
<input type="email" name="email" class="form-control" required id="email" value="{{.Email}}"
aria-describedby="emailValid">
</div>
{{if .EmailErr}}
<small id="emailValid" style="color: #f44336;" class="form-text">{{.EmailErr}}</small>
{{end}}
</div>
{{end}}
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">名称</span>
</div>
<input type="text" name="username" class="form-control" required id="username" value="{{.Username}}"
aria-describedby="usernameValid">
</div>
{{if .UsernameErr}}
<small id="usernameValid" style="color: #f44336;" class="form-text">{{.UsernameErr}}</small>
{{end}}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">密码</span>
</div>
<input type="password" name="password" class="form-control" required id="password" value="{{.Password}}"
aria-describedby="passwordValid">
</div>
{{if .PasswordErr}}
<small id="passwordValid" style="color: #f44336;" class="form-text">{{.PasswordErr}}</small>
{{end}}
</div>
<button type="submit" class="btn btn-primary btn-block">提交</button>
</form>
{{if .Summary}}
<div class="py-md-5" style="color: #f44336;">
{{.Summary}}
</div>
{{end}}
</div>
</div>
</div>
{{template "footer" .}}

View File

@ -0,0 +1,126 @@
{{template "header" .}}
<div class="container-fluid flex justify-content">
<div class="main">
<div class="title">
<h3>上传视频</h3>
<a href="/me/videos" class="btn btn-primary">返回列表</a>
</div>
<div class="col-sm-6 py-md-5 flex flex-column justify-content">
<form action="/me/videos/create" method="post">
{{if .ID}}
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">ID</span>
</div>
<input type="text" class="form-control" disabled value="{{.ID}}">
<input type="hidden" value="{{.ID}}" name="id">
</div>
</div>
{{end}}
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">标题</span>
</div>
<input type="text" name="title" class="form-control" required id="title" value="{{.Title}}"
aria-describedby="titleValid">
</div>
{{if .TitleErr}}
<small id="titleValid" style="color: #f44336;" class="form-text">{{.TitleErr}}</small>
{{end}}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">图片</span>
</div>
<input type="file" class="form-control" id="upload_images">
<input type="button" class="btn btn-secondary" value="上传" onclick="uploadImage()" />
<input type="hidden" id="images" name="images" required value="{{.Images}}" aria-describedby="imagesValid">
</div>
<div class="image-box" style="margin-top: 8px;">
{{if .Images}}
<img width="120px" src="{{.Images}}" />
{{end}}
</div>
{{if .ImagesErr}}
<small id="imagesValid" style="color: #f44336;" class="form-text">{{.ImagesErr}}</small>
{{end}}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">描述</span>
</div>
<textarea class="form-control" id="description" name="description" required
aria-describedby="descriptionValid" rows="3">{{.Description}}</textarea>
</div>
{{if .DescriptionErr}}
<small id="descriptionValid" style="color: #f44336;" class="form-text">{{.DescriptionErr}}</small>
{{end}}
</div>
<div class="form-group">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">视频</span>
</div>
<input type="file" class="form-control" id="upload_video">
<input type="button" class="btn btn-secondary" value="上传" onclick="uploadVideo()" />
<input type="hidden" id="video" name="origin_link" required value="{{.OriginLink}}">
</div>
<div class="video-box" style="margin-top: 8px;">
{{if .OriginLink}}
<p>{{.OriginLink}}</p>
{{end}}
</div>
<small id="upload_video_msg" style="color: #f44336;" class="form-text">
{{.OriginLinkErr}}
</small>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
</div>
</div>
{{define "js"}}
<script>
function uploadImage() {
var files = $('#upload_images').prop('files');
var data = new FormData();
data.append('file', files[0]);
$.ajax({
url: '/upload_image',
type: 'POST',
data: data,
cache: false,
processData: false,
contentType: false,
success: function (res) {
$('#images').val(res);
$('.image-box').append('<img width="120px" src="' + res + '" />');
}
})
}
function uploadVideo() {
var files = $('#upload_video').prop('files');
var data = new FormData();
data.append('file', files[0]);
$.ajax({
url: '/upload_file',
type: 'POST',
data: data,
cache: false,
processData: false,
contentType: false,
success: function (res) {
$('#video').val(res);
$('#upload_video_msg').html('上传成功')
}
})
}
</script>
{{end}}
{{template "footer" .}}

View File

@ -1,14 +1,25 @@
{{template "header" .}}
<div class="container-fluid flex justify-content">
<div class="main">
<h6 style="margin-top: 20px;margin-bottom: 10px;">正在播放
{{if eq .Video.Status 200}}
<h6 style="margin-top: 20px;margin-bottom: 10px;">正在播放
<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
<path d="M607.698947 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.79603-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L599.899033 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099504 3.299964L190.903534 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596031 416.695414c11.599872 13.399853 31.79965 14.799837 45.099504 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z" p-id="6425"></path><path d="M864.696118 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.796029-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L856.896204 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099503 3.299964L447.900705 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596032 416.695414c11.599872 13.399853 31.79965 14.799837 45.099503 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z" p-id="6426"></path>
</svg>Git - Div Rhino<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
<path d="M416.301053 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.79603-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L424.100967 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099504 3.299964l360.596031 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596031 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099504 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z" p-id="6274"></path><path d="M159.303882 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.796029-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L167.103796 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099503 3.299964l360.596032 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596032 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099503 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z" p-id="6275"></path>
<path
d="M607.698947 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.79603-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L599.899033 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099504 3.299964L190.903534 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596031 416.695414c11.599872 13.399853 31.79965 14.799837 45.099504 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z"
p-id="6425"></path>
<path
d="M864.696118 992.000352c0-7.399919-2.599971-14.899836-7.799914-20.89977l-360.796029-416.99541c-20.999769-23.999736-20.999769-60.299336 0-84.299073l0.099999-0.099999L856.896204 52.910688c11.599872-13.399853 10.099889-33.59963-3.299964-45.099504-13.399853-11.599872-33.59963-10.099889-45.099503 3.299964L447.900705 427.806562c-20.399775 23.299744-31.599652 53.199414-31.599652 84.199073s11.199877 60.89933 31.599652 84.199073l360.596032 416.695414c11.599872 13.399853 31.79965 14.799837 45.099503 3.299964 7.399919-6.299931 11.099878-15.199833 11.099878-24.199734z"
p-id="6426"></path>
</svg>{{.Video.Title}}<svg class="icon" viewBox="0 0 1024 1024" width="15" height="21">
<path
d="M416.301053 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.79603-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L424.100967 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099504 3.299964l360.596031 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596031 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099504 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z"
p-id="6274"></path>
<path
d="M159.303882 992.000352c0-7.399919 2.599971-14.899836 7.799914-20.89977l360.796029-416.895412c20.999769-23.999736 20.999769-60.299336 0-84.299072l-0.099999-0.099999L167.103796 52.910688c-11.599872-13.399853-10.099889-33.59963 3.299964-45.099504 13.399853-11.599872 33.59963-10.099889 45.099503 3.299964l360.596032 416.695414c20.399775 23.299744 31.599652 53.199414 31.599652 84.199073s-11.199877 60.89933-31.599652 84.199073l-360.596032 416.695414c-11.599872 13.399853-31.79965 14.799837-45.099503 3.299964-7.399919-6.299931-11.099878-15.199833-11.099878-24.199734z"
p-id="6275"></path>
</svg>
</h6>
<video id="video" controls width="100%"></video>
<video id="video" controls width="100%" style="background-color: #000000;"></video>
<div class="tip-box">
<div class="tip-icon">
<svg focusable="false" aria-hidden="true" viewBox="0 0 24 24" data-testid="InfoOutlinedIcon">
@ -22,22 +33,27 @@
<p>如果播放遇到问题,请尝试切换播放源。</p>
</div>
</div>
<div style="width: 100%; height: 600px;background-color: antiquewhite;"></div>
{{else}}
<div class="">该视频目前还不能播放!</div>
{{end}}
</div>
</div>
{{define "js"}}
<script src="/statics/js/hls.min.js"></script>
<script>
var video = document.getElementById('video');
if (Hls.isSupported()) {
console.log("supported");
var hls = new Hls();
hls.loadSource('{{.Url}}');
hls.loadSource('{{.Video.PlayLink}}');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
//video.play();
});
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
console.log("no supported");
video.src = '{{.Url}}';
video.src = '{{.Video.PlayLink}}';
video.addEventListener('loadedmetadata', function () {
//video.play();
});
@ -45,7 +61,10 @@
// reurn false 禁止函数内部执行其他的事件或者方法
var vol = 0.1; // 1代表100%音量每次增减0.1
var time = 10; // 单位秒每次增减10秒
document.onkeyup = function (event) {//键盘事件
// 键盘事件
document.onkeyup = function (event) {
const nodeName = event.target.nodeName;
if (nodeName && (nodeName.toUpperCase() === "INPUT" || nodeName.toUpperCase() === "TEXTAREA")) { return; }
// console.log("keyCode:" + event.keyCode);
var e = event || window.event || arguments.callee.caller.arguments[0];
// 鼠标上下键控制视频音量
@ -66,10 +85,19 @@
video.volume !== video.duration ? video.currentTime += time : 1;
return false;
} else if (e && e.keyCode === 32) {
// if (e.preventDefault) {
// e.preventDefault();
// } else {
// window.event.returnValue = false;
// }
// event.preventDefault();
// event.returnValue = false;
// 按空格键 判断当前是否暂停
// alert(1)
video.paused === true ? video.play() : video.pause();
return false;
}
}
</script>
{{end}}
{{template "footer" .}}

View File

@ -0,0 +1,43 @@
{{template "header" .}}
<div class="container-fluid flex justify-content">
<div class="main">
<div class="title">
<h3>视频转码</h3>
</div>
<div>
<h5>{{.Video.Title}}</h5>
<p>{{.Video.Description}}</p>
<p><img src="{{.Video.Images}}" width="120px" /></p>
{{if eq .Video.Status 0}}
<p>该视频需要转码才能播放</p>
<button id="transfer" class="btn btn-primary">转码</button>
{{else if eq .Video.Status 1}}
<p>转码中...</p>
{{else if eq .Video.Status 2}}
<p>转码失败, 请联系管理员</p>
{{else if eq .Video.Status 200}}
<p>恭喜您, 转码成功!</p>
{{end}}
<p id="msg"></p>
</div>
</div>
</div>
{{define "js"}}
<script>
$('#transfer').click(function () {
let that = $(this)
that.attr("disable", true).html('转码中...')
$.ajax({
url: '/transfer/{{.Video.ID}}',
type: 'post',
success: function (obj) {
$('#msg').html(obj)
},
error: function (ex) {
console.log(ex)
}
});
});
</script>
{{end}}
{{template "footer" .}}