VideoTools/internal/convert/dvd_regions.go
Stu Leak d45d16f89b Implement DVD-NTSC encoding support with multi-region capabilities
Add comprehensive DVD-Video encoding functionality:

- New internal/convert package with modular architecture
  - types.go: Core types (VideoSource, ConvertConfig, FormatOption)
  - ffmpeg.go: FFmpeg codec mapping and video probing
  - presets.go: Output format definitions
  - dvd.go: NTSC-specific DVD encoding and validation
  - dvd_regions.go: PAL, SECAM, and multi-region support

- New internal/app/dvd_adapter.go for main.go integration

Features implemented:
✓ DVD-NTSC preset (720×480@29.97fps, MPEG-2/AC-3)
✓ Multi-region support (NTSC, PAL, SECAM - all region-free)
✓ Comprehensive validation system with actionable warnings
✓ Automatic framerate conversion (23.976p, 24p, 30p, 60p)
✓ Audio resampling to 48 kHz
✓ Aspect ratio handling (4:3, 16:9, letterboxing)
✓ Interlacing detection and preservation
✓ DVDStyler-compatible output (no re-encoding)
✓ PS2-safe bitrate limits (max 9000 kbps)

Complete technical specifications and integration guide in:
- DVD_IMPLEMENTATION_SUMMARY.md

All packages compile without errors or warnings.
Ready for integration with existing queue and UI systems.

🤖 Generated with Claude Code
2025-11-29 19:30:05 -05:00

289 lines
9.1 KiB
Go

package convert
import (
"fmt"
"strings"
)
// DVDRegion represents a DVD standard/region combination
type DVDRegion string
const (
DVDNTSCRegionFree DVDRegion = "ntsc-region-free"
DVDPALRegionFree DVDRegion = "pal-region-free"
DVDSECAMRegionFree DVDRegion = "secam-region-free"
)
// DVDStandard represents the technical specifications for a DVD encoding standard
type DVDStandard struct {
Region DVDRegion
Name string
Resolution string // "720x480" or "720x576"
FrameRate string // "29.97" or "25.00"
VideoFrames int // 30 or 25
AudioRate int // 48000 Hz (universal)
Type string // "NTSC", "PAL", or "SECAM"
Countries []string
DefaultBitrate string // "6000k" for NTSC, "8000k" for PAL
MaxBitrate string // "9000k" for NTSC, "9500k" for PAL
AspectRatios []string
InterlaceMode string // "interlaced" or "progressive"
Description string
}
// GetDVDStandard returns specifications for a given DVD region
func GetDVDStandard(region DVDRegion) *DVDStandard {
standards := map[DVDRegion]*DVDStandard{
DVDNTSCRegionFree: {
Region: DVDNTSCRegionFree,
Name: "DVD-Video NTSC (Region-Free)",
Resolution: "720x480",
FrameRate: "29.97",
VideoFrames: 30,
AudioRate: 48000,
Type: "NTSC",
Countries: []string{"USA", "Canada", "Japan", "Brazil", "Mexico", "Australia", "New Zealand"},
DefaultBitrate: "6000k",
MaxBitrate: "9000k",
AspectRatios: []string{"4:3", "16:9"},
InterlaceMode: "interlaced",
Description: `NTSC DVD Standard
Resolution: 720x480 pixels
Frame Rate: 29.97 fps (30000/1001)
Bitrate: 6000-9000 kbps
Audio: AC-3 Stereo, 48 kHz, 192 kbps
Regions: North America, Japan, Australia, and others`,
},
DVDPALRegionFree: {
Region: DVDPALRegionFree,
Name: "DVD-Video PAL (Region-Free)",
Resolution: "720x576",
FrameRate: "25.00",
VideoFrames: 25,
AudioRate: 48000,
Type: "PAL",
Countries: []string{"Europe", "Africa", "Asia (except Japan)", "Australia", "New Zealand", "Argentina", "Brazil"},
DefaultBitrate: "8000k",
MaxBitrate: "9500k",
AspectRatios: []string{"4:3", "16:9"},
InterlaceMode: "interlaced",
Description: `PAL DVD Standard
Resolution: 720x576 pixels
Frame Rate: 25.00 fps
Bitrate: 8000-9500 kbps
Audio: AC-3 Stereo, 48 kHz, 192 kbps
Regions: Europe, Africa, most of Asia, Australia, New Zealand`,
},
DVDSECAMRegionFree: {
Region: DVDSECAMRegionFree,
Name: "DVD-Video SECAM (Region-Free)",
Resolution: "720x576",
FrameRate: "25.00",
VideoFrames: 25,
AudioRate: 48000,
Type: "SECAM",
Countries: []string{"France", "Russia", "Greece", "Eastern Europe", "Central Asia"},
DefaultBitrate: "8000k",
MaxBitrate: "9500k",
AspectRatios: []string{"4:3", "16:9"},
InterlaceMode: "interlaced",
Description: `SECAM DVD Standard
Resolution: 720x576 pixels
Frame Rate: 25.00 fps
Bitrate: 8000-9500 kbps
Audio: AC-3 Stereo, 48 kHz, 192 kbps
Regions: France, Russia, Eastern Europe, Central Asia
Note: SECAM DVDs are technically identical to PAL in the DVD standard (color encoding differences are applied at display time)`,
},
}
return standards[region]
}
// PresetForRegion creates a ConvertConfig preset for the specified DVD region
func PresetForRegion(region DVDRegion) ConvertConfig {
std := GetDVDStandard(region)
if std == nil {
// Fallback to NTSC
std = GetDVDStandard(DVDNTSCRegionFree)
}
// Determine resolution as string
var resStr string
if std.Resolution == "720x576" {
resStr = "720x576"
} else {
resStr = "720x480"
}
return ConvertConfig{
SelectedFormat: FormatOption{Name: std.Name, Label: std.Name, Ext: ".mpg", VideoCodec: "mpeg2video"},
Quality: "Standard (CRF 23)",
Mode: "Advanced",
VideoCodec: "MPEG-2",
EncoderPreset: "medium",
BitrateMode: "CBR",
VideoBitrate: std.DefaultBitrate,
TargetResolution: resStr,
FrameRate: std.FrameRate,
PixelFormat: "yuv420p",
HardwareAccel: "none",
AudioCodec: "AC-3",
AudioBitrate: "192k",
AudioChannels: "Stereo",
InverseTelecine: false,
AspectHandling: "letterbox",
OutputAspect: "source",
}
}
// ValidateForDVDRegion performs comprehensive validation for a specific DVD region
func ValidateForDVDRegion(src *VideoSource, region DVDRegion) []DVDValidationWarning {
std := GetDVDStandard(region)
if std == nil {
std = GetDVDStandard(DVDNTSCRegionFree)
}
var warnings []DVDValidationWarning
if src == nil {
warnings = append(warnings, DVDValidationWarning{
Severity: "error",
Message: "No video source selected",
Action: "Cannot proceed without a source video",
})
return warnings
}
// Add standard information
warnings = append(warnings, DVDValidationWarning{
Severity: "info",
Message: fmt.Sprintf("Encoding for: %s", std.Name),
Action: fmt.Sprintf("Resolution: %s @ %s fps", std.Resolution, std.FrameRate),
})
// 1. Target Resolution Validation
var targetWidth, targetHeight int
if strings.Contains(std.Resolution, "576") {
targetWidth, targetHeight = 720, 576
} else {
targetWidth, targetHeight = 720, 480
}
if src.Width != targetWidth || src.Height != targetHeight {
warnings = append(warnings, DVDValidationWarning{
Severity: "info",
Message: fmt.Sprintf("Input resolution is %dx%d (target: %dx%d)", src.Width, src.Height, targetWidth, targetHeight),
Action: fmt.Sprintf("Will scale to %dx%d with aspect-ratio correction", targetWidth, targetHeight),
})
}
// 2. Framerate Validation
if src.FrameRate > 0 {
var expectedRate float64
if std.Type == "NTSC" {
expectedRate = 29.97
} else {
expectedRate = 25.0
}
normalized := normalizeFrameRate(src.FrameRate)
switch {
case isFramerateClose(src.FrameRate, expectedRate):
// Good
case std.Type == "NTSC" && (normalized == "23.976" || normalized == "24.0"):
warnings = append(warnings, DVDValidationWarning{
Severity: "warning",
Message: fmt.Sprintf("Input framerate is %.2f fps (23.976p/24p)", src.FrameRate),
Action: "Will apply 3:2 pulldown to convert to 29.97fps",
})
case std.Type == "NTSC" && (normalized == "59.94" || normalized == "60.0"):
warnings = append(warnings, DVDValidationWarning{
Severity: "warning",
Message: fmt.Sprintf("Input framerate is %.2f fps (59.94p/60p)", src.FrameRate),
Action: "Will decimate to 29.97fps",
})
case normalized == "vfr":
warnings = append(warnings, DVDValidationWarning{
Severity: "error",
Message: "Input is Variable Frame Rate (VFR)",
Action: fmt.Sprintf("Will force constant frame rate at %s fps", std.FrameRate),
})
default:
warnings = append(warnings, DVDValidationWarning{
Severity: "warning",
Message: fmt.Sprintf("Input framerate is %.2f fps (standard is %s fps)", src.FrameRate, std.FrameRate),
Action: fmt.Sprintf("Will convert to %s fps", std.FrameRate),
})
}
}
// 3. Audio Sample Rate
if src.AudioRate > 0 && src.AudioRate != 48000 {
warnings = append(warnings, DVDValidationWarning{
Severity: "warning",
Message: fmt.Sprintf("Audio sample rate is %d Hz (not 48 kHz)", src.AudioRate),
Action: "Will resample to 48 kHz (DVD standard)",
})
}
// 4. Interlacing Analysis
if !src.IsProgressive() {
warnings = append(warnings, DVDValidationWarning{
Severity: "info",
Message: "Input is interlaced",
Action: "Will preserve interlacing (optimal for DVD)",
})
} else {
warnings = append(warnings, DVDValidationWarning{
Severity: "info",
Message: "Input is progressive",
Action: "Will encode as progressive",
})
}
// 5. Bitrate Safety Check
warnings = append(warnings, DVDValidationWarning{
Severity: "info",
Message: fmt.Sprintf("Bitrate range: %s (recommended) to %s (maximum PS2-safe)", std.DefaultBitrate, std.MaxBitrate),
Action: "Using standard bitrate settings for compatibility",
})
// 6. Aspect Ratio Information
validAspects := std.AspectRatios
warnings = append(warnings, DVDValidationWarning{
Severity: "info",
Message: fmt.Sprintf("Supported aspect ratios: %s", strings.Join(validAspects, ", ")),
Action: "Output will preserve source aspect or apply specified handling",
})
return warnings
}
// isFramerateClose checks if a framerate is close to an expected value
func isFramerateClose(actual, expected float64) bool {
diff := actual - expected
if diff < 0 {
diff = -diff
}
return diff < 0.1 // Within 0.1 fps
}
// parseMaxBitrate extracts the numeric bitrate from a string like "9000k"
func parseMaxBitrate(s string) int {
var bitrate int
fmt.Sscanf(strings.TrimSuffix(s, "k"), "%d", &bitrate)
return bitrate
}
// ListAvailableDVDRegions returns information about all available DVD encoding regions
func ListAvailableDVDRegions() []DVDStandard {
regions := []DVDRegion{DVDNTSCRegionFree, DVDPALRegionFree, DVDSECAMRegionFree}
var standards []DVDStandard
for _, region := range regions {
if std := GetDVDStandard(region); std != nil {
standards = append(standards, *std)
}
}
return standards
}