Chapter 7 ยท CORE

Media Preview Pipeline

๐Ÿ“„ 07_media_preview_pipeline.md ๐Ÿท Core

Chapter 7: Media Preview Pipeline

Welcome to the final chapter of our core backend tutorial!

In Chapter 6: Frontend View Layer, we built a beautiful interface that displays lists of files. However, there is one big problem: Everything looks the same.

Whether it is a 4K movie, a PDF contract, or a vacation photo, the user just sees a generic icon. To see the content, they have to download the whole file.

In this chapter, we are building the Media Preview Pipeline. This is a specialized "Darkroom" inside our server that takes massive files, extracts a single frame or page, and creates a tiny image (thumbnail) that loads instantly.

The Problem: The Bandwidth Trap

Imagine you have a folder with 50 video files, each 1GB in size.

The Solution: The Pipeline

The pipeline is a series of steps:

  1. Identify: What is this file? (Video, Image, PDF?)
  2. Check Cache: Have we already made a thumbnail for this?
  3. Process: Use the right tool (like FFmpeg for video) to take a snapshot.
  4. Resize: Shrink it down.
  5. Store: Save the result so we don't have to do it again.

Key Concepts

1. The Service (preview package)

This is the manager. It decides which tool to use. If you ask for a preview of party.mp4, the Service knows to call the Video Processor. If you ask for photo.jpg, it calls the Image Resizer.

2. FFmpeg (The Heavy Lifter)

For videos, we use an external tool called FFmpeg. It is a command-line program that can "seek" to a specific second in a video and take a screenshot.

3. The Cache (DiskCache)

Generating a thumbnail from a video takes a lot of CPU power. We never want to do it twice. Once we generate an image, we save it in a hidden folder (e.g., .filebrowser/cache/thumbnails). Next time, we serve the file from the cache instantly.


How it Works: A Use Case

Let's look at the scenario: A user scrolls down to see movie.mp4.

  1. Request: The frontend asks for a "small" preview of movie.mp4.
  2. Lookup: The Service checks if cache/movie_mp4_small.jpg exists.
  3. Generation: It doesn't exist. The Service tells FFmpeg: "Go to the 10% mark of this video and take a picture."
  4. Result: FFmpeg returns raw image bytes.
  5. Response: The server sends the image to the browser.

Usage Example

Here is how the HTTP handler (from Chapter 3) talks to the Preview Service.

// backend/http/preview.go (Conceptual)

func getPreview(file iteminfo.ExtendedFileInfo) ([]byte, error) {
    // 1. Get the global preview service
    service := preview.GetService()

    // 2. Ask for a preview (Size: "small", Seek: 10%)
    // This function handles caching and generation automatically.
    imgData, err := preview.GetPreviewForFile(
        ctx, 
        file, 
        "small", // Preview size
        "",      // URL (unused for local files)
        10       // Seek percentage (10% into the video)
    )

    return imgData, err
}

Explanation:


Under the Hood: The Processing Flow

How does the code decide what to do? It follows a strict decision tree.

The Flow Diagram

sequenceDiagram participant H as Handler participant S as Service participant C as Cache participant F as FFmpeg participant I as Image Resizer H->>S: GetPreview(movie.mp4) S->>C: Check Cache Key (MD5) alt Cache Hit C-->>S: Return Image Bytes else Cache Miss S->>S: Determine Type (Video) S->>F: Extract Frame at 10% F-->>S: Return Raw Image S->>I: Resize to 256x256 I-->>S: Return Thumbnail S->>C: Save to Disk end S-->>H: Return Final Image

Deep Dive: Determining the Strategy

In backend/preview/preview.go, the generateRawPreview function acts as the traffic cop.

// backend/preview/preview.go

func (s *Service) generateRawPreview(ctx context.Context, file iteminfo.ExtendedFileInfo, ...) ([]byte, error) {
    // 1. Figure out what kind of file this is
    previewType := determinePreviewType(file)

    switch previewType {
    case previewTypeImage:
        // Use standard Go image library
        return s.generateImagePreview(ctx, file, previewSize)

    case previewTypeVideo:
        // Use FFmpeg external command
        return s.generateVideoPreviewBytes(ctx, file, seekPercentage)
    
    case previewTypeDocument:
        // Use PDF library
        return s.generateDocumentPreview(ctx, file, hash)
        
    default:
        return nil, errors.New("unknown preview type")
    }
}

Explanation:

Deep Dive: Video Processing with FFmpeg

Generating a video thumbnail is the most complex part. We use the ffmpeg package to run a command.

// backend/ffmpeg/video.go

func (s *FFmpegService) GenerateVideoPreviewStreaming(ctx context.Context, path string, ...) error {
    // 1. Construct the command
    // "ffmpeg -ss [time] -i [video] -frames 1 -f image2 -"
    cmd := exec.CommandContext(ctx, s.ffmpegPath,
        "-ss", seekTimeStr, // Jump to specific second
        "-i", videoPath,    // Input file
        "-vcodec", "mjpeg", // Output format
        "-",                // Write to Standard Output (RAM)
    )

    // 2. Run the command and capture the image data
    cmd.Stdout = writer
    return cmd.Run()
}

Explanation:


Concurrency: The Bouncer

What happens if 100 users try to generate previews for 100 different 4K movies at the exact same time? The server would crash. FFmpeg uses a lot of CPU.

To prevent this, the preview package uses Semaphores (channels). Think of this as a nightclub bouncer.

// backend/preview/preview.go

// Create a channel that can hold only X items (e.g., 4)
imageSem = make(chan struct{}, 4)

func (s *Service) acquireImageSem(ctx context.Context) error {
    select {
    case s.imageSem <- struct{}{}: 
        // 1. Success! We entered the "club". Proceed to processing.
        return nil
    case <-ctx.Done():
        // 2. Timeout or Cancelled.
        return ctx.Err()
    }
}

Explanation: Before starting FFmpeg, the code calls acquireImageSem.

This ensures that your Raspberry Pi or server doesn't freeze when you open a folder full of movies.


Caching: The Memory

Finally, we need to ensure we don't repeat work. We use an MD5 hash to create a unique ID for every file state.

// backend/preview/preview.go

func GeneratePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, ...) {
    // 1. Create a unique string based on Path + Size + ModTime
    cacheString := fmt.Sprintf("%s:%d:%s", file.RealPath, file.Size, file.ModTime)
    
    // 2. Hash it to get a filename (e.g., "a1b2c3d4...")
    hasher := md5.New()
    hasher.Write([]byte(cacheString))
    cacheHash := hex.EncodeToString(hasher.Sum(nil))

    // 3. Check if this hash exists on disk
    if data, found, _ := service.fileCache.Load(ctx, cacheHash); found {
        return data, nil // Return cached image immediately
    }

    // 4. If not found, start generation...
}

Explanation: We include ModTime (Modification Time) in the hash. If you edit a photo or replace a video, the ModTime changes, the hash changes, and the system automatically generates a new preview. You never see an old thumbnail for a new file.


Summary

In this final chapter, we turned our file manager into a rich media gallery.

  1. The Preview Service acts as a smart dispatcher, routing files to the correct processors.
  2. FFmpeg Integration allows us to peek inside video files without downloading them.
  3. Concurrency Control ensures the server stays responsive even under heavy load.
  4. Caching ensures that browsing is snappy and efficient.

Conclusion

Congratulations! You have explored the entire architecture of Filebrowser.

You now have a deep understanding of how a modern, full-stack Go application is structured. Happy coding!


Generated by Code IQ