Files
questshock/modding/hacked/ss1/content/movie/Read.go
T

212 lines
6.4 KiB
Go

package movie
import (
"bytes"
"encoding/binary"
"io"
"github.com/inkyblackness/hacked/ss1"
"github.com/inkyblackness/hacked/ss1/content/bitmap"
"github.com/inkyblackness/hacked/ss1/content/movie/internal/compression"
"github.com/inkyblackness/hacked/ss1/content/movie/internal/format"
"github.com/inkyblackness/hacked/ss1/content/text"
"github.com/inkyblackness/hacked/ss1/resource"
"github.com/inkyblackness/hacked/ss1/serial"
)
const (
errSourceIsNil ss1.StringError = "source is nil"
errInvalidFormat ss1.StringError = "not a MOVI format"
errLowResVideoNotSupported ss1.StringError = "low-res video not supported"
)
// Read tries to extract a MOVI container from the provided reader.
// On success the position of the reader is past the last data entry.
// On failure the position of the reader is undefined.
func Read(source io.ReadSeeker, cp text.Codepage) (Container, error) {
if source == nil {
return Container{}, errSourceIsNil
}
var header format.Header
startPos, _ := source.Seek(0, io.SeekCurrent)
var container Container
err := binary.Read(source, binary.LittleEndian, &header)
if err != nil {
return Container{}, err
}
err = verifyAndExtractHeader(&header, &container)
if err != nil {
return Container{}, err
}
var startPalette bitmap.Palette
err = readPalette(source, &startPalette)
if err != nil {
return Container{}, err
}
entries, err := readIndexAndEntries(source, startPos, &header)
if err != nil {
return Container{}, err
}
err = parseEntries(entries, &container,
cp, startPalette, format.Timestamp{Second: byte(header.Duration.Number), Fraction: header.Duration.Fraction})
if err != nil {
return Container{}, err
}
return container, nil
}
func verifyAndExtractHeader(header *format.Header, container *Container) error {
if !bytes.Equal(header.Tag[:], bytes.NewBufferString(format.Tag).Bytes()) {
return errInvalidFormat
}
container.Video.Height = header.VideoHeight
container.Video.Width = header.VideoWidth
container.Audio.Sound.SampleRate = header.AudioSampleRate.ToFloat()
return nil
}
func readPalette(source io.Reader, pal *bitmap.Palette) error {
decoder := serial.NewDecoder(source)
decoder.Code(pal)
return decoder.FirstError()
}
func readIndexAndEntries(source io.ReadSeeker, startPos int64, header *format.Header) ([]format.Entry, error) {
entries := make([]format.Entry, 0, header.IndexEntryCount)
indexEntries := make([]format.IndexTableEntry, header.IndexEntryCount)
err := binary.Read(source, binary.LittleEndian, indexEntries)
if err != nil {
return nil, err
}
for index, indexEntry := range indexEntries {
entryType := format.DataType(indexEntry.Type)
if entryType != format.DataTypeEndOfMedia {
timestamp := format.Timestamp{
Second: indexEntry.TimestampSecond,
Fraction: indexEntry.TimestampFraction,
}
length := int(indexEntries[index+1].DataOffset - indexEntry.DataOffset)
_, err = source.Seek(startPos+int64(indexEntry.DataOffset), io.SeekStart)
if err != nil {
return nil, err
}
entryData, err := readEntry(entryType, source, length)
if err != nil {
return nil, err
}
entries = append(entries, format.Entry{
Timestamp: timestamp,
Data: entryData,
})
}
}
return entries, nil
}
func readEntry(entryType format.DataType, source io.Reader, dataSize int) (format.EntryData, error) {
limited := io.LimitReader(source, int64(dataSize))
switch entryType {
case format.DataTypeLowResVideo:
return format.LowResVideoEntryFrom(limited, dataSize)
case format.DataTypeHighResVideo:
return format.HighResVideoEntryFrom(limited, dataSize)
case format.DataTypeAudio:
return format.AudioEntryFrom(limited, dataSize)
case format.DataTypeSubtitle:
return format.SubtitleEntryFrom(limited, dataSize)
case format.DataTypePalette:
return format.PaletteEntryFrom(limited)
case format.DataTypePaletteReset:
return format.PaletteResetEntryFrom()
case format.DataTypePaletteLookupList:
return format.PaletteLookupEntryFrom(limited, dataSize)
case format.DataTypeControlDictionary:
return format.ControlDictionaryEntryFrom(limited, dataSize)
default:
return format.UnknownEntryFrom(entryType, limited, dataSize)
}
}
func parseEntries(entries []format.Entry, container *Container,
cp text.Codepage, startPalette bitmap.Palette, endTimestamp format.Timestamp) error {
palette := startPalette
var paletteLookup []byte
var controlDictionary []compression.ControlWord
var highResScene *HighResScene
sceneChanging := true
finishScene := func() {
if highResScene != nil {
container.Video.Scenes = append(container.Video.Scenes, *highResScene)
}
highResScene = nil
}
var frame *HighResFrame
finishFrame := func(timestamp format.Timestamp) {
if frame != nil {
frame.displayTime = timestamp.Minus(frame.displayTime)
highResScene.frames = append(highResScene.frames, *frame)
}
frame = nil
}
for _, entry := range entries {
switch data := entry.Data.(type) {
case format.AudioEntryData:
container.Audio.Sound.Samples = append(container.Audio.Sound.Samples, data.Samples...)
case format.SubtitleEntryData:
subText := cp.Decode(data.Text)
switch data.Control {
case format.SubtitleTextStd:
container.Subtitles.add(resource.LangDefault, entry.Timestamp.ToDuration(), subText)
case format.SubtitleTextGer:
container.Subtitles.add(resource.LangGerman, entry.Timestamp.ToDuration(), subText)
case format.SubtitleTextFrn:
container.Subtitles.add(resource.LangFrench, entry.Timestamp.ToDuration(), subText)
case format.SubtitleArea:
default:
}
case format.LowResVideoEntryData:
// ignored for now
return errLowResVideoNotSupported
case format.PaletteLookupEntryData:
sceneChanging = true
paletteLookup = data.List
case format.ControlDictionaryEntryData:
sceneChanging = true
controlDictionary = data.Words
case format.PaletteResetEntryData:
sceneChanging = true
palette = bitmap.Palette{}
case format.PaletteEntryData:
sceneChanging = true
palette = data.Colors
case format.HighResVideoEntryData:
finishFrame(entry.Timestamp)
if sceneChanging {
finishScene()
highResScene = &HighResScene{
palette: palette,
paletteLookup: paletteLookup,
controlWords: controlDictionary,
}
sceneChanging = false
}
frame = &HighResFrame{
bitstream: data.Bitstream,
maskstream: data.Maskstream,
displayTime: entry.Timestamp,
}
default:
}
}
finishFrame(endTimestamp)
finishScene()
return nil
}