MODULE TV;
IMPORT Kernel, Machine, KernelLog, Commands, Streams, Dates, Strings,
TVDriver, WMRectangles, Graphics := WMGraphics, Messages := WMMessages, Files, TVChannels,
Standard := WMStandardComponents,
TeletextDecoder, WMRestorable, XML, Modules, WMDialogs,
WM := WMWindowManager;
CONST
ChannelFile = TVChannels.ChannelFile;
SwitchingInterval = 5;
TYPE
ChannelSeeker = OBJECT
VAR
dead : BOOLEAN;
seeking : BOOLEAN;
stepSize : LONGINT;
tuner : TVDriver.TVTuner;
audio : TVDriver.Audio;
sigFound, sigLost : LONGINT;
PROCEDURE &Init*(tuner : TVDriver.TVTuner; audio : TVDriver.Audio);
BEGIN
SELF.tuner := tuner;
dead := FALSE;
seeking := FALSE;
END Init;
PROCEDURE SetStepSize(stepSize : LONGINT);
BEGIN {EXCLUSIVE}
SELF.stepSize := stepSize;
END SetStepSize;
PROCEDURE StartSeeking;
BEGIN {EXCLUSIVE}
seeking := TRUE;
END StartSeeking;
PROCEDURE StopSeeking;
BEGIN {EXCLUSIVE}
seeking := FALSE;
END StopSeeking;
PROCEDURE SeekChannel;
BEGIN {EXCLUSIVE}
audio.SetAudioMute;
REPEAT
tuner.SetTVFrequency(tuner.GetFrequency() + stepSize);
UNTIL ~seeking OR dead OR ~tuner.IsLocked();
REPEAT
tuner.SetTVFrequency(tuner.GetFrequency() + stepSize);
UNTIL ~seeking OR dead OR tuner.IsLocked();
sigFound := tuner.GetFrequency();
REPEAT
tuner.SetTVFrequency(tuner.GetFrequency() + stepSize);
UNTIL ~seeking OR dead OR ~tuner.IsLocked();
sigLost := tuner.GetFrequency();
tuner.SetTVFrequency((sigFound + sigLost) DIV 2);
seeking := FALSE;
audio.SetAudioUnmute;
END SeekChannel;
PROCEDURE Release;
BEGIN {EXCLUSIVE}
dead := TRUE;
END Release;
BEGIN {ACTIVE}
BEGIN {EXCLUSIVE}
REPEAT
AWAIT(dead OR seeking);
IF ~dead THEN
SeekChannel;
END;
UNTIL dead;
END;
END ChannelSeeker;
TvWindow* = OBJECT(WM.BufferWindow);
VAR
vcd : TVDriver.VideoCaptureDevice;
tuner : TVDriver.TVTuner;
audio : TVDriver.Audio;
vbi : TeletextDecoder.VbiDecoder;
timer : Kernel.Timer;
chNr : LONGINT;
chSwitcher : Standard.Timer;
autoSwitch, alive- : BOOLEAN;
chnlSeeker : ChannelSeeker;
newImage : BOOLEAN;
lastX, lastY : LONGINT;
dragging : BOOLEAN;
chName : ARRAY 33 OF CHAR;
vcdNr* : LONGINT;
nextINchain : TvWindow;
PROCEDURE &New*(vcd : TVDriver.VideoCaptureDevice);
VAR
ch : TVChannels.TVChannel;
BEGIN
ASSERT(vcd # NIL);
SELF.vcd := vcd;
vcdNr := -1;
tuner := vcd.GetTuner();
audio := vcd.GetAudio();
audio.SetAudioIntern();
Init(640, 480, FALSE);
SetTitle(WM.NewString("TV"));
NEW(chnlSeeker, tuner, audio);
vcd.VideoOpen();
vcd.InstallNotificationHandler(SELF.NewImage);
vcd.SetInputDev1;
tuner.Open();
tuner.SetChannel(2);
tuner.InstallChannelSwitchHandler (SELF.HandleSwitch);
IF (TVChannels.channels # NIL) & (TVChannels.channels.GetCount() > 0) THEN
ch := TVChannels.channels.GetItem (0);
tuner.SetTVFrequency(ch.freq);
END;
audio.SetAudioIntern;
audio.SetAudioUnmute;
vcd.SetVideo(Machine.PhysicalAdr(SELF.img.adr, 0), SELF.img.bpr);
vcd.SetGeometry(640, 480, 1, {});
vcd.SetPixelFormat(2);
vcd.CaptureContinuous;
manager := WM.GetDefaultManager();
manager.Add(100, 100, SELF, {WM.FlagFrame});
SetPointerInfo(manager.pointerNull);
nextINchain := windows;
windows := SELF
END New;
PROCEDURE Close;
VAR
vbiBuffer : TVDriver.VbiBuffer;
BEGIN
BEGIN {EXCLUSIVE}
alive := FALSE
END;
IF chSwitcher # NIL THEN
chSwitcher.Stop(NIL, NIL)
END;
chnlSeeker.Release;
chnlSeeker := NIL;
alive := FALSE;
IF vcd.IsVbiOpen() THEN
tuner.CloseVbi();
vbiBuffer := vcd.GetVbiBuffer();
vbiBuffer.Finalize
END;
IF vbi # NIL THEN
vbi.Stop
END;
vcd.VideoClose;
tuner.Close;
Close^;
FreeWindow(SELF)
END Close;
PROCEDURE StartAutoSwitch*;
BEGIN
IF chSwitcher = NIL THEN
NEW(chSwitcher);
chSwitcher.interval.Set(SwitchingInterval * 60 * 1000);
chSwitcher.onTimer.Add(NextCh)
END;
chSwitcher.Start(NIL, NIL);
autoSwitch := TRUE;
KernelLog.String("{TV} Automatic channel switch enabled."); KernelLog.Ln
END StartAutoSwitch;
PROCEDURE StopAutoSwitch*;
BEGIN
chSwitcher.Stop(NIL, NIL);
autoSwitch := FALSE;
KernelLog.String("{TV} Automatic channel switch stopped."); KernelLog.Ln
END StopAutoSwitch;
PROCEDURE NextCh(sender, par : ANY);
VAR
ch : TVChannels.TVChannel;
BEGIN
REPEAT
chNr := (chNr + 1) MOD TVChannels.channels.GetCount();
ch := TVChannels.channels.GetItem (chNr)
UNTIL ch.hasTeletext;
tuner.SetTVFrequency(ch.freq)
END NextCh;
PROCEDURE PointerDown*(x, y : LONGINT; keys : SET);
BEGIN
lastX := bounds.l+x; lastY := bounds.t+y; dragging := TRUE
END PointerDown;
PROCEDURE PointerMove*(x,y : LONGINT; keys : SET);
VAR dx, dy : LONGINT;
BEGIN
IF dragging THEN
x := bounds.l + x; y := bounds.t + y; dx := x - lastX; dy := y - lastY;
lastX := lastX + dx; lastY := lastY + dy;
IF (dx # 0) OR (dy # 0) THEN manager.SetWindowPos(SELF, bounds.l + dx, bounds.t + dy) END
END
END PointerMove;
PROCEDURE PointerUp*(x, y : LONGINT; Keys : SET);
BEGIN
dragging := FALSE
END PointerUp;
PROCEDURE Draw*(canvas : Graphics.Canvas; w, h, q : LONGINT);
BEGIN
Draw^(canvas, w, h, 0)
END Draw;
PROCEDURE Handle(VAR m : Messages.Message);
VAR
data : XML.Element;
str : ARRAY 10 OF CHAR;
BEGIN
IF m.msgType = Messages.MsgKey THEN
KeyEvent(m.x, m.flags, m.y)
ELSIF m.msgType = Messages.MsgPointer THEN
IF m.msgSubType = Messages.MsgSubPointerMove THEN PointerMove(m.x, m.y, m.flags)
ELSIF m.msgSubType = Messages.MsgSubPointerDown THEN PointerDown(m.x, m.y, m.flags)
ELSIF m.msgSubType = Messages.MsgSubPointerUp THEN PointerUp(m.x, m.y, m.flags)
ELSIF m.msgSubType = Messages.MsgSubPointerLeave THEN PointerLeave
END
ELSIF m.msgType = Messages.MsgClose THEN Close
ELSIF m.msgType = Messages.MsgStyleChanged THEN StyleChanged
ELSIF (m.msgType = Messages.MsgExt) & (m.ext # NIL) THEN
IF (m.ext IS WMRestorable.Storage) THEN
NEW(data); data.SetName("TVData");
Strings.IntToStr(vcdNr, str);
data.SetAttributeValue("device", str);
Strings.IntToStr(tuner.GetFrequency(), str);
data.SetAttributeValue("freq", str);
IF autoSwitch THEN
data.SetAttributeValue("autoSwitch", "true")
ELSE
data.SetAttributeValue("autoSwitch", "false")
END;
m.ext(WMRestorable.Storage).Add("TV", "TV.Restore", SELF, data)
ELSE Handle^(m)
END
ELSE Handle^(m)
END;
END Handle;
PROCEDURE HandleSwitch (freq : LONGINT; tuner : TVDriver.TVTuner);
VAR
i : LONGINT;
ch : TVChannels.TVChannel;
title : ARRAY 32 OF CHAR;
BEGIN
IF TVChannels.channels.GetCount() = 0 THEN RETURN END;
IF SELF.tuner # tuner THEN
RETURN
END;
i := 0;
REPEAT
ch := TVChannels.channels.GetItem (i);
INC (i)
UNTIL (i = TVChannels.channels.GetCount()) OR ((ch.freq-10 < freq) & (ch.freq + 10 > freq));
title := "TV";
IF (ch.freq-10 < freq) & (ch.freq + 10 > freq) THEN
Strings.Append (title, " - ");
COPY(ch.name, chName);
Strings.Append (title, chName)
ELSE
Strings.Append (title, " - unregistered channel")
END;
SetTitle (WM.NewString (title));
IF vbi # NIL THEN
vbi.ResetAll;
vbi.SetFrequency (freq)
END
END HandleSwitch;
PROCEDURE StartTeletextCapture*;
VAR
status : LONGINT;
BEGIN
IF ~vcd.IsVbiOpen() THEN
status := tuner.OpenVbi();
IF status # 0 THEN
KernelLog.String("{TV} Could not open Vbi device."); KernelLog.Ln;
RETURN
END;
NEW(vbi, vcd)
END
END StartTeletextCapture;
PROCEDURE StopTeletextCapture*;
BEGIN
tuner.CloseVbi;
IF vbi # NIL THEN
vbi.Stop;
vbi := NIL
END
END StopTeletextCapture;
PROCEDURE KeyEvent(ucs : LONGINT; flags : SET; keySym : LONGINT);
VAR
ch : CHAR;
BEGIN
ch := CHR(ucs);
ch := Strings.LOW(ch);
IF ch = "+" THEN
chnlSeeker.StopSeeking;
chnlSeeker.SetStepSize(5);
chnlSeeker.StartSeeking;
ELSIF ch = "-" THEN
chnlSeeker.StopSeeking;
chnlSeeker.SetStepSize(-5);
chnlSeeker.StartSeeking;
ELSIF ch = 'a' THEN
StartAutoSwitch
ELSIF ch = 'q' THEN
StopAutoSwitch
ELSIF ch = 'c' THEN
IF vbi # NIL THEN
KernelLog.String("{TV} ");
KernelLog.String(chName);
KernelLog.String(" Teletext contains ");
KernelLog.Int(vbi.Count(), 0);
KernelLog.String(" pages.");
KernelLog.Ln
END
ELSIF ch = 't' THEN
StartTeletextCapture
ELSIF ch = 'e' THEN
StopTeletextCapture
ELSIF ch = "s" THEN
chnlSeeker.StopSeeking;
ELSIF ch = "n" THEN
tuner.SetChannel(tuner.GetChannel()+1);
ELSIF ch = "p" THEN
tuner.SetChannel(tuner.GetChannel()-1);
ELSIF ch = "1" THEN
vcd.StopCaptureContinuous;
vcd.SetGeometry(320, 240, 1, {});
vcd.CaptureContinuous;
ELSIF ch = "2" THEN
vcd.StopCaptureContinuous;
vcd.SetGeometry(640, 480, 1, {});
vcd.CaptureContinuous;
ELSIF ch = "m" THEN
IF audio.IsAudioMute() THEN audio.SetAudioUnmute
ELSE audio.SetAudioMute
END
END;
END KeyEvent;
PROCEDURE GetTVFreq*() : LONGINT;
BEGIN
RETURN tuner.GetFrequency()
END GetTVFreq;
PROCEDURE NewImage;
BEGIN {EXCLUSIVE}
newImage := TRUE;
END NewImage;
BEGIN {ACTIVE}
alive := TRUE; NEW(timer);
WHILE alive DO
BEGIN {EXCLUSIVE}
AWAIT(newImage OR ~alive);
newImage := FALSE
END;
IF alive THEN
Invalidate(WMRectangles.MakeRect(0, 0, img.width, img.height))
END
END
END TvWindow;
VAR
windows : TvWindow;
PROCEDURE BuildChannelTable* (context : Commands.Context);
VAR filename : ARRAY 100 OF CHAR;
BEGIN
IF context.arg.GetString(filename) THEN
BuildChannelTableImpl(filename);
END;
END BuildChannelTable;
PROCEDURE BuildChannelTableImpl(filename : ARRAY OF CHAR);
CONST
Delay = 500;
VAR
ch, max, i, found, length, res : LONGINT;
tvWnd : TvWindow;
tvCh : TVChannels.TVChannel;
channels : TVChannels.ChannelList;
suite : TeletextDecoder.TeletextSuite;
vcd : TVDriver.VideoCaptureDevice;
tuner : TVDriver.TVTuner;
timer : Kernel.Timer;
f : Files.File;
w : Files.Writer;
fileBak, lastName : ARRAY 33 OF CHAR;
doc : XML.Document;
chList, channel : XML.Element;
comment : ARRAY 100 OF CHAR;
xmlComment : XML.Comment;
freq : ARRAY 10 OF CHAR;
BEGIN
vcd := TVDriver.GetDefaultDevice();
IF vcd = NIL THEN
KernelLog.String("{TV} BuildChannelTable: Fail to locate video capture device."); KernelLog.Ln;
RETURN
END;
IF vcd.IsVideoOpen() THEN
KernelLog.String("{TV} BuildChannelTable: Close TV window before channel installation."); KernelLog.Ln;
RETURN
END;
NEW(tvWnd, vcd);
tuner := vcd.GetTuner();
IF tuner.OpenVbi() = 0 THEN
NEW(tvWnd.vbi, vcd);
tvWnd.vbi.extractName := TRUE
END;
NEW (TVChannels.channels);
channels := TVChannels.channels;
TeletextDecoder.teletextSuites := NIL;
max := tuner.GetMaxChannel();
KernelLog.String("{TV} Automatic channel installation "); KernelLog.Ln;
KernelLog.String("{TV} This will take about ");
KernelLog.Int(max*Delay DIV 1000, 0);
KernelLog.String(" seconds."); KernelLog.Ln;
found := 0;
NEW (timer);
FOR ch := 0 TO max-1 DO
tuner.SetChannel (ch);
IF TeletextDecoder.SelectTeletextSuite(tuner.GetFrequency()) = NIL THEN
NEW (tvCh);
tvCh.freq := tuner.GetFrequency();
NEW (suite);
suite.channel := tvCh;
suite.next := TeletextDecoder.teletextSuites;
TeletextDecoder.teletextSuites := suite;
timer.Sleep (Delay);
FOR i := 0 TO 12 DO
tvCh.name[i] := tvWnd.vbi.chName[i]
END;
IF (Strings.Length (tvCh.name) = 0) OR (Strings.Match(tvCh.name, lastName)) THEN
TeletextDecoder.teletextSuites := TeletextDecoder.teletextSuites.next
ELSE
INC(found);
channels.Add(tvCh);
COPY (tvCh.name, lastName)
END
END
END;
KernelLog.String("{TV} Automatic channel installation done.");
KernelLog.Ln;
KernelLog.String("{TV} Found "); KernelLog.Int(found, 0); KernelLog.String(" channels");
KernelLog.Ln;
tvWnd.Close;
IF found > 0 THEN
f := Files.Old (ChannelFile);
IF f # NIL THEN
COPY(ChannelFile, filename);
fileBak := ChannelFile;
length := Strings.Length (fileBak);
fileBak[length-3] := 'B';
fileBak[length-2] := 'a';
fileBak[length-1] := 'k';
Files.Rename (filename, fileBak, res);
IF res # 0 THEN
KernelLog.String("{TV} Error backing up existing channel file.");
KernelLog.Ln
END;
KernelLog.String("{TV} Original file was backed up to '");
KernelLog.String(fileBak);
KernelLog.String("'");
KernelLog.Ln
END;
NEW(doc);
NEW(xmlComment);
comment := "TV channels; Auto-generated settings.";
xmlComment.SetStr(comment);
doc.AddContent(xmlComment);
NEW(xmlComment);
Strings.DateToStr(Dates.Now(), comment);
Strings.Concat("This file was created on ", comment, comment);
xmlComment.SetStr(comment);
doc.AddContent(xmlComment);
NEW(chList); chList.SetName("TVChannelList");
FOR i := 0 TO channels.GetCount() -1 DO
tvCh := channels.GetItem(i);
Strings.IntToStr(tvCh.freq, freq);
NEW(channel); channel.SetName("Channel");
channel.SetAttributeValue("name", tvCh.name);
channel.SetAttributeValue("freq", freq);
chList.AddContent(channel)
END;
doc.AddContent(chList);
IF filename = "" THEN
COPY(ChannelFile, filename)
END;
f := Files.New (filename);
Files.OpenWriter (w, f, 0);
doc.Write(w, NIL, 0);
w.Update;
Files.Register (f)
END
END BuildChannelTableImpl;
PROCEDURE Open*(context : Commands.Context);
VAR
tvWnd : TvWindow;
vcd : TVDriver.VideoCaptureDevice;
devNr : LONGINT;
param : ARRAY 32 OF CHAR;
BEGIN
IF context # NIL THEN
IF context.arg.GetInteger(devNr, FALSE) THEN
context.out.String("{TV} Open device #"); context.out.Int(devNr, 0); context.out.Ln;
vcd := TVDriver.GetVideoDevice(devNr);
context.arg.SkipWhitespace; context.arg.String(param);
ELSE
vcd := TVDriver.GetDefaultDevice()
END
ELSE
vcd := TVDriver.GetDefaultDevice()
END;
IF vcd = NIL THEN
IF (context # NIL) & (context.arg.res = 0) THEN
context.error.String("{TV} Parameter is not a valid video device number."); context.error.Ln;
WMDialogs.Error("TV - Error", "Parameter is not a valid video device number");
RETURN;
ELSE
context.error.String("{TV} Cannot open TV window: Fail to locate video capture device.");
context.error.Ln;
WMDialogs.Error("TV - Error",
"Cannot open TV window: Fail to locate video capture device. Install device before opening the TV window. Example: BT848.Install");
RETURN;
END
END;
IF ~ vcd.IsVideoOpen() THEN
NEW(tvWnd, vcd);
IF devNr # -1 THEN
tvWnd.vcdNr := devNr
END;
Strings.UpperCase(param);
IF param = 'TXT' THEN
tvWnd.StartTeletextCapture;
tvWnd.StartAutoSwitch
END
END;
END Open;
PROCEDURE Restore*(context : WMRestorable.Context);
VAR
manager : WM.WindowManager;
xml : XML.Element;
s : Strings.String;
i : LONGINT;
vcd : TVDriver.VideoCaptureDevice;
tuner : TVDriver.TVTuner;
tvWnd : TvWindow;
BEGIN
IF context # NIL THEN
IF context.appData # NIL THEN
xml := context.appData(XML.Element);
s := xml.GetAttributeValue("device");
IF s # NIL THEN
Strings.StrToInt(s^, i);
IF i = -1 THEN
vcd := TVDriver.GetDefaultDevice()
ELSE
vcd := TVDriver.GetVideoDevice(i)
END;
IF (vcd # NIL) & (~ vcd.IsVideoOpen()) THEN
NEW(tvWnd, vcd);
manager := WM.GetDefaultManager();
manager.Remove(tvWnd)
END
END;
s := xml.GetAttributeValue("freq");
IF (s # NIL) & (vcd # NIL) THEN
tuner := vcd.GetTuner();
Strings.StrToInt(s^, i);
tuner.SetTVFrequency(i)
END;
s := xml.GetAttributeValue("autoSwitch");
IF (s # NIL) & (s^ = "true") & (tvWnd # NIL) THEN
tvWnd.StartAutoSwitch
END;
IF tvWnd # NIL THEN
WMRestorable.AddByContext(tvWnd, context)
ELSE
KernelLog.String("{TV} Could not restore the TV window."); KernelLog.Ln;
IF vcd = NIL THEN
KernelLog.String("{TV} Install the device driver first, e.g. BT848.Install"); KernelLog.Ln
ELSE
KernelLog.String("{TV} The selected TV window is already open."); KernelLog.Ln
END
END
END
END
END Restore;
PROCEDURE FreeWindow(tvWnd : TvWindow);
VAR
w : TvWindow;
BEGIN
IF tvWnd = NIL THEN
RETURN
ELSIF tvWnd = windows THEN
windows := windows.nextINchain
ELSE
w := windows;
WHILE (w # NIL) & (w.nextINchain # tvWnd) DO
w := w.nextINchain
END;
IF w # NIL THEN
w.nextINchain := tvWnd.nextINchain
END
END
END FreeWindow;
PROCEDURE Cleanup;
VAR
w : TvWindow;
BEGIN
w := windows;
WHILE w # NIL DO
w.Close;
w := w.nextINchain
END;
windows := NIL
END Cleanup;
BEGIN
IF TVChannels.channels.GetCount() = 0 THEN
KernelLog.String("{TV} No channels detected. Performing auto-detection.");
KernelLog.Ln;
BuildChannelTableImpl (ChannelFile)
END;
Modules.InstallTermHandler(Cleanup)
END TV.
SystemTools.Free TV ~
Usage: TV.Open [ [deviceNr] TXT ] ~
Requires driver installation, e.g. BT848.Install ~
and valid TVChannels.XML file.