August'24: Kamaelia is in maintenance mode and will recieve periodic updates, about twice a year, primarily targeted around Python 3 and ecosystem compatibility. PRs are always welcome. Latest Release: 1.14.32 (2024/3/24)
Find the code for this here:
/Code/Python/Kamaelia/Examples/DVB_Systems/RecordNamedChannel.py
Recording a channel from a DVB (digital video broadcasting) broadcast is relatively simple if you know the packet IDs (PIDs) for packets containing the audio and video streams of the service (the channel) you want to record. But what if you only know the channel's name?
In this example the component DVB_TuneToChannel uses various DVB components to extract and parse the Program Specific Information (PSI) tables needed to work it out itself. It therefore needs to be able to talk to the DVB Receiver to request packets with varous PIDs as it realises it needs them.
The top level of the system is therefore this:
from Kamaelia.Chassis.Graphline import Graphline
from Kamaelia.File.Writing import SimpleFileWriter
import dvb3.frontend
feparams = {
"inversion" : dvb3.frontend.INVERSION_AUTO,
"constellation" : dvb3.frontend.QAM_16,
"coderate_HP" : dvb3.frontend.FEC_3_4,
"coderate_LP" : dvb3.frontend.FEC_3_4,
}
from Kamaelia.Device.DVB.Receiver import Receiver
RegisterService( Receiver(505833330.0/1000000.0, feparams),
{"MUX1":"inbox"}
).activate()
Pipeline( DVB_TuneToChannel(channel="BBC ONE",fromDemuxer="MUX1"),
SimpleFileWriter("bbc_one.ts"),
).run()
The DVB Receiver component (which contains both a tuner and
demuxer) is registered as a named service "MUX1".
DVB_TuneToChannel is given its name, so it can request packets
with specific PIDs.
DVB_TuneToChannel is then pipelined with a
SimpleFileWriter so that audio and video packets it eventually
outputs will be written to a file.
The component takes a channel name, and the name of the demuxer service as arguments:
class DVB_TuneToChannel(AdaptiveCommsComponent): ... def __init__(self, channel, fromDemuxer): super(DVB_TuneToChannel,self).__init__() self.channelname = channel self.demuxerservice = fromDemuxer
First, DVB_TuneToChannel resolves the demuxer service name it was given and adds an outbox and linkage to allow it to send requests to it:
def main(self):
# get the demuxer service
toDemuxer = self.addOutbox("toDemuxer")
cat = CAT.getcat()
service = cat.retrieveService(self.demuxerservice)
self.link((self,toDemuxer),service)
Before they can be parsed, PSI tables need to be reconstructed from the transport stream packets they are carried in. So next it sets up a named service "PSI" to reconstruct them, based on a ReassemblePSITablesService component. It is linked to the demuxer service so it can requests packets with the PIDs it needs:
psi = ReassemblePSITablesService()
psi_service = RegisterService(psi,{"PSI":"request"}).activate()
self.link( (psi,"pid_request"), service )
The first step is to resolve the service name "BBC ONE" in this example, to the service's id. This data is held in the Service Description Table (SDT), which is carried in a fixed PID. So DVB_TuneToChannel creates the correct parsing component and subscribes it to the PSI table service; then adds an inbox and links it to receive output from the pipeline:
sdt_parser = Pipeline( Subscribe("PSI", [SDT_PID]),
ParseServiceDescriptionTable_ActualTS()
).activate()
fromSDT = self.addInbox("fromSDT")
fromSDT_linkage = self.link( (sdt_parser,"outbox"),(self,fromSDT) )
DVB_TuneToChannel then waits for the parsing component to
return a table and searches it for the matching service ID and transport
stream ID:
service_id = None
while service_id == None:
while not self.dataReady(fromSDT):
self.pause()
yield 1
sdt_table = self.recv(fromSDT)
transport_stream_id = sdt_table['transport_stream_id']
# see if we can find our services channel name
for (sid,service) in sdt_table['services'].items():
for (dtype,descriptor) in service['descriptors']:
if descriptor['type'] == "service":
if descriptor['service_name'].lower() == self.channelname.lower():
service_id = sid
break
print "Found service id:",service_id
print "Its in transport stream id:",transport_stream_id
The PIDs for the audio and video streams are recorded in a Program
Map Table (PMT). There is one of these for each service in the
multiplex. The PIDs for these are listed in the Program Association
Table (PAT) which is carried in packets with a known PID.
So the next steps are to examine the Program Association Table, then find and examine the correct Program Map Table for the service we want.
As was done for the Service Description table, DVB_TuneToChannel sets up a parser for the Program Association Table, and an inbox to collect the results:
pat_parser = Pipeline( Subscribe("PSI", [PAT_PID]),
ParseProgramAssociationTable()
).activate()
fromPAT = self.addInbox("fromPAT")
fromPAT_linkage = self.link( (pat_parser,"outbox"),(self,fromPAT) )
It then waits for a parsed table to be returned and searches it for
the PID for packets containing the Program Map Table for the
service:
# wait until we get data back from the PAT
PMT_PID = None
while PMT_PID == None:
while not self.dataReady(fromPAT):
self.pause()
yield 1
sdt_table = self.recv(fromPAT)
# see if we can find our service's PMT
ts_services = sdt_table['transport_streams'][transport_stream_id]
if service_id in ts_services:
PMT_PID = ts_services[service_id]
break
print "Found PMT PID for this service:",PMT_PID
Nowthe PID for packets containing the Program Map Table is known,
DVB_TuneToChannel can set up a parser that table:
pmt_parser = Pipeline( Subscribe("PSI", [PMT_PID]),
ParseProgramMapTable()
).activate()
fromPMT = self.addInbox("fromPMT")
fromPMT_linkage = self.link( (pmt_parser,"outbox"),(self,fromPMT) )
It then waits for the parsed table to be returned and searches for the first PIDs it can find for an audio and a video stream:
# wait until we get data back from the PMT
audio_pid = None
video_pid = None
while audio_pid == None and video_pid == None:
while not self.dataReady(fromPMT):
self.pause()
yield 1
pmt_table = self.recv(fromPMT)
if service_id in pmt_table['services']:
service = pmt_table['services'][service_id]
for stream in service['streams']:
if stream['type'] in [3,4] and not audio_pid:
audio_pid = stream['pid']
elif stream['type'] in [1,2] and not video_pid:
video_pid = stream['pid']
print "Found audio PID:",audio_pid
print "Found video PID:",video_pid
Now the PIDs for packets containing the service's audio and video are known, the final step is for DVB_TuneToChannel to request to be sent packets with those PIDs; so they can be sent on out of its "outbox" outbox (to go to the file writer component):
fromDemuxer = self.addInbox("fromDemuxer")
self.send( ("ADD",[audio_pid,video_pid], (self,fromDemuxer)), toDemuxer)
while 1:
while self.dataReady(fromDemuxer):
packet = self.recv(fromDemuxer)
self.send(packet,"outbox")
self.pause()
yield 1
So there you have it. DVB_TuneToChannel uses various parsing
components that are part of Kamaelia to extract and interpret the
Program Specific Information tables available in a DVB multiplex
carrying an MPEG transport stream.
-- 04 Jan 2007, Matt