使用Golang为网站构建视频编辑功能的后端

使用Golang为网站构建视频编辑功能的后端 大家好,

我正在开发一个视频编辑网站,并使用Go构建后端。虽然大部分功能都已实现,但我遇到了一些技术挑战:

  1. 使用Go进行视频处理: 我正在使用 ffmpeg-go 来处理视频处理任务,如剪辑、合并和添加转场。然而,对于较大的视频,处理时间明显高于预期。是否有办法优化Go的视频处理,或者我应该考虑其他方法,例如并行运行FFmpeg任务?

  2. 实时视频预览: 我希望生成实时视频预览(例如,在用户应用滤镜或进行编辑后)。Go是否适合高效处理此类任务,还是我需要将其卸载到专门的服务器?如果Go可以处理,您推荐哪些库或模式?

  3. 处理大文件上传: 用户经常上传大型视频文件(有时超过2GB)。我正在使用基于Go的服务器配合多部分表单处理器,但上传偶尔会失败,尤其是对于较大的文件或在较慢的连接上。在Go中处理大文件上传以确保可靠性和性能的最佳方法是什么?

  4. 用于进度更新的WebSocket集成: 我已经实现了WebSocket来通知用户其视频处理任务的进度,但更新不一致。有时服务器会在任务中途停止发送进度更新。Go的WebSocket库是否存在已知问题,或者这可能是我实现的问题?

  5. 为前端优化API: 我的前端(使用React构建)经常与Go后端交互以获取视频元数据和更新编辑内容。随着更多用户加入平台,API调用的延迟正在增加。如何优化我的Go API以处理高流量,同时保持快速的响应时间?

我很乐意听取任何有使用Go构建视频密集型平台经验的人的建议、最佳实践或示例。任何帮助都将不胜感激!

提前感谢!


更多关于使用Golang为网站构建视频编辑功能的后端的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

有人能帮我解决这个问题吗?我非常希望得到您的指导。谢谢!

更多关于使用Golang为网站构建视频编辑功能的后端的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


joeroot:

在Go中处理大文件上传,同时确保可靠性和性能的最佳方法是什么?

我还没有测试过,但理论上这应该可行。

  1. 创建一个S3对象存储(不仅AWS提供此服务)
  2. 将文档上传到你的S3存储桶
  3. 将路径和元数据存储在关系型数据库中

S3对象存储可以处理TB级数据,并且或许可以使用gRPC进行流式传输?

第五点基本上是我的专长,而且看起来确实有些不对劲。这个网站有多少用户?即使没有优化,你的后端也应该能够处理大量的连接和用户。 如果这是“你的个人网站”,那根本不应该出现这个问题,Go 天生就很快。

此外,你已经集成了 WebSockets,但仍然有很多 API 调用。(这里信息太少,无法判断,但也许值得思考一下)

最后,你的 GitHub 账户显示的是另一个人,可能人们因此不在这里留言;他们以为你是个机器人 机器人表情

ffmpeg-go:可以尝试 GitHub - asticode/go-astiav: Golang ffmpeg and libav C bindingsGitHub - 3d0c/gmf: Go Media Framework

这两个库我都用过。它们是集成度很好的库。如果你使用静态库编译,运行环境就不需要ffmpeg相关的共享库。 还有一些其他工具库是通过命令行调用的,但在我看来,它们非常糟糕。

对于其他问题,我认为其他人已经回答得差不多了,我就不再补充了。

我重新阅读了你的问题,我认为我应该再次回答:

  1. ffmpeg-go,这个问题和我之前说的一样。
  2. 视频预览,这和第一个问题一样,调用一些库方法来处理可以达到目的。至于效率问题:ffmpeg库可以使用硬件加速,这取决于视频类型和运行环境,请自行了解(但纯软件计算也不会太差,至少在我功耗相对较低的CPU结合OpenGL的情况下,我的GUI屏幕仍然可以显示60帧解码后的视频。)
  3. 对于大文件上传,除了拥有可靠的后端存储,你还需要在前端和后端添加重传机制,以便在传输失败时进行断点续传。这和你的下载逻辑类似。
  4. websocket?如果只是为了更新进度,我认为ws有点重(如果你没有双向通信或大量复杂API的需求,那么不要使用ws,它只会增加你的维护成本);ws库,据我所知,在普通使用中没有明显的缺陷,但不排除你遇到了bug。
  5. 看起来你只是没有很好地调度处理逻辑。这是一个复杂的主题。在看到完整代码之前,我无法指出具体问题。但有一些常见的概念。最简单的优化是调整处理逻辑,减少互斥等。

joeroot:

  • 与 ffmpeg-go 集成:视频上传到 S3 后,您建议将它们下载到后端服务器进行处理,还是直接在存储服务上处理更好?如果是后者,是否有特定的工具或库可以让 FFmpeg 与 S3 高效协作?

这超出了我的能力范围,但听起来网络服务器应该在发送到 S3 服务器之前进行转换。

  • 用于流式传输的 gRPC:您能澄清一下在这种情况下如何利用 gRPC吗?它是用于在后端和 S3 之间流式传输视频数据,还是用于向前端通信进度更新?

我打算做的是一个“微服务”(gRPC 服务器,几乎像一个 API),它只处理网络服务器和 S3 存储之间的通信。或许它可以在将文件传递给存储之前处理转换和其他处理。

  • 元数据存储:当您说“将路径和元数据存储在 RDBMS 中”时,您对如何构建这个有偏好吗?例如,我是否应该将文件版本(例如,原始、已处理、预览)存储在一个具有不同路径的表中,还是使用更规范化的结构?

还没到那一步,但我想我应该将元数据存储在 RDBMS 中以实现快速搜索。并且搜索标准可能因情况而异。

感谢您的建议!

使用S3兼容的对象存储来处理大文件上传听起来是一个可靠的方法。我理解将文件存储委托给像S3这样的服务可以减轻Go服务器的负载并提高可扩展性。

关于这种方法,我有几个后续问题:

  1. 与 ffmpeg-go 集成:视频上传到S3后,您建议将它们下载到后端服务器进行处理,还是直接在存储服务上进行处理更好?如果是后者,是否有特定的工具或库可以让FFmpeg高效地与S3协同工作?此外,我注意到像CapCut这样的视频编辑器能高效执行高级处理任务——您认为类似的优化方法是否适用于服务器端视频编辑流水线?对于那些有兴趣探索像CapCut中那样的高级编辑功能的人,可以点击这里查看CapCut修改版apk版本。
  2. 用于流式传输的 gRPC:您能详细说明在这种情况下如何利用gRPC吗?它是用于在后端和S3之间流式传输视频数据,还是用于向前端通信进度更新?
  3. 元数据存储:当您说“将路径和元数据存储在RDBMS中”时,对于如何构建这个结构有偏好吗?例如,我应该在单个表中存储文件版本(例如,原始、已处理、预览)并附带不同的路径,还是使用更规范化的结构?

再次感谢您的意见!我将进一步探索这个方向,看看它如何能优化我的平台。

1. 使用Go进行视频处理优化

对于大型视频处理,建议采用并行FFmpeg任务处理。使用Go的goroutine池可以显著提升处理效率。以下是示例代码:

package main

import (
    "context"
    "fmt"
    "github.com/u2takey/ffmpeg-go"
    "golang.org/x/sync/errgroup"
)

func processVideoSegment(inputPath, outputPath string, start, duration float64) error {
    err := ffmpeg_go.Input(inputPath, ffmpeg_go.KwArgs{"ss": start}).
        Output(outputPath, ffmpeg_go.KwArgs{"t": duration, "c": "copy"}).
        OverWriteOutput().
        Run()
    return err
}

func parallelVideoProcessing(segments []VideoSegment) error {
    g, ctx := errgroup.WithContext(context.Background())
    g.SetLimit(4) // 限制并发数

    for _, seg := range segments {
        seg := seg
        g.Go(func() error {
            select {
            case <-ctx.Done():
                return ctx.Err()
            default:
                return processVideoSegment(seg.Input, seg.Output, seg.Start, seg.Duration)
            }
        })
    }
    return g.Wait()
}

type VideoSegment struct {
    Input    string
    Output   string
    Start    float64
    Duration float64
}

2. 实时视频预览处理

Go适合处理实时视频预览,推荐使用gstreamer绑定库。以下是示例:

package main

import (
    "github.com/notedit/gstreamer-go"
    "net/http"
)

func generatePreview(w http.ResponseWriter, r *http.Request) {
    pipelineStr := "filesrc location=input.mp4 ! decodebin ! videoconvert ! jpegenc ! multipartmux boundary=frame ! appsink name=sink"
    
    pipeline, err := gstreamer.New(pipelineStr)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    sink := pipeline.GetByName("sink")
    w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
    
    for {
        sample := sink.PullSample()
        if sample == nil {
            break
        }
        w.Write(sample.Data())
        w.(http.Flusher).Flush()
    }
}

3. 大文件上传处理

使用分块上传和流式处理可以解决大文件上传问题:

package main

import (
    "io"
    "net/http"
    "os"
    "path/filepath"
)

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(32 << 20) // 32MB内存缓冲
    
    file, header, err := r.FormFile("video")
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    defer file.Close()
    
    // 创建临时文件
    tempFile, err := os.CreateTemp("uploads", "upload-*.tmp")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer tempFile.Close()
    
    // 流式写入
    buf := make([]byte, 1024*1024) // 1MB缓冲区
    for {
        n, err := file.Read(buf)
        if err != nil && err != io.EOF {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        if n == 0 {
            break
        }
        
        if _, err := tempFile.Write(buf[:n]); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
    }
    
    // 重命名为最终文件
    finalPath := filepath.Join("uploads", header.Filename)
    os.Rename(tempFile.Name(), finalPath)
    
    w.WriteHeader(http.StatusCreated)
}

4. WebSocket进度更新

使用gorilla/websocket库实现稳定的进度更新:

package main

import (
    "log"
    "net/http"
    "time"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

func progressHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    defer conn.Close()
    
    // 模拟处理进度
    for i := 0; i <= 100; i += 10 {
        time.Sleep(1 * time.Second)
        
        err := conn.WriteJSON(map[string]interface{}{
            "progress": i,
            "status":   "processing",
        })
        if err != nil {
            log.Println("Write error:", err)
            break
        }
        
        // 添加ping/pong保持连接
        conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
    }
    
    conn.WriteJSON(map[string]interface{}{
        "progress": 100,
        "status":   "completed",
    })
}

5. API性能优化

使用缓存、连接池和异步处理优化API性能:

package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "net/http"
    "sync"
    "time"
    "github.com/go-redis/redis/v8"
    _ "github.com/lib/pq"
)

var (
    rdb *redis.Client
    db  *sql.DB
    cache sync.Map
)

func init() {
    rdb = redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
        PoolSize: 100, // 连接池大小
    })
    
    db, _ = sql.Open("postgres", "user=postgres dbname=video_edit sslmode=disable")
    db.SetMaxOpenConns(50)
}

func getVideoMetadata(w http.ResponseWriter, r *http.Request) {
    videoID := r.URL.Query().Get("id")
    
    // 检查内存缓存
    if val, ok := cache.Load(videoID); ok {
        json.NewEncoder(w).Encode(val)
        return
    }
    
    // 检查Redis缓存
    ctx := context.Background()
    val, err := rdb.Get(ctx, "video:"+videoID).Result()
    if err == nil {
        var data map[string]interface{}
        json.Unmarshal([]byte(val), &data)
        cache.Store(videoID, data)
        json.NewEncoder(w).Encode(data)
        return
    }
    
    // 数据库查询
    row := db.QueryRowContext(ctx, 
        "SELECT id, title, duration FROM videos WHERE id = $1", videoID)
    
    var video Video
    if err := row.Scan(&video.ID, &video.Title, &video.Duration); err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    
    // 缓存结果
    videoJSON, _ := json.Marshal(video)
    rdb.Set(ctx, "video:"+videoID, videoJSON, 10*time.Minute)
    cache.Store(videoID, video)
    
    json.NewEncoder(w).Encode(video)
}

type Video struct {
    ID       string  `json:"id"`
    Title    string  `json:"title"`
    Duration float64 `json:"duration"`
}
回到顶部