Cookbook : Recording named programmes from a DVB broadcastFind the code for this here: So we can record a whole broadcast, or a single channel determined by its name; but what about recording individual programmes? recordForMe componentWe can create a component to handle each recording we want to make.The recordForMe component takes a channel name, programme name, and filename and will record anything broadcast on that channel with that name:
What does recordForMe need to do:
The recordForMe component is therefore a simple pipeline:
def recordForMe(channel, programme, filename): It does the following:
Supporting services: demuxer, PSI tables, & channel namesThe supporting services are implemented as named services that components can talk to. Demuxing packets from the broadcastThe most basic service needed is the ability to request to be sent packets (with specific IDs) that have been received from the broadcast stream:
The Receiver component is a combined tuner and demulipliexer service, capable of handling requests from multiple client components asking to be sent packets. Its "inbox" inbox is registered as a named service for clients to access it by. Note that this system is hard wired to tune to a single broadcast multiplex. If the programmes you want to record are on a different channel, then its tough luck! Reconstruction of Program Specific Information (PSI) tablesPSI tables, containing information about the broadcast stream, will be needed by several parts of the system. The ReassemblePSITablesService component can provide this as a service that other components can subscribe to:
from Kamaelia.Device.DVB.Parse.ReassemblePSITables import ReassemblePSITablesService The component is linked to the "DEMUXER" service so it can request packets as it needs them to be able to service requests for tables from clients. Its "request" inbox is then registered as a named service for clients to access it by. 'Now' event informationWe are now in a position to extract and parse the Event Information Tables (EIT), process it down to individual events for when programmes start, and make them available on a "nowEvent" Backplane:
Pipeline( Subscribe("PSI_Tables", [EIT_PID]), The steps involved, above, are:
Looking up channel namesThe final service needed is one for mapping a channel name to its numeric service ID. Again, this is implemented as a named service:
The mappings needed are in the Service Description Table, so that table is requested from the "PSI_Tables" reconstruction service and is parsed by an ParseServiceDescriptionTable_ActualTS component.RegisterService( \ ChannelNameLookupService component is fed the parsed tables, and keeps a note of the most recent, so it can perform lookups when requested by clients. The key aspects of the ChannelNameLookupService component is its main loop, for handling requests and receiving new tables: def main(self): ... and how it resolves the channel name to its corresponding service ID and transport stream ID: def lookup(self, channelname): The rest of the code for this component handles adding and removing subscribers. Client subscribe, rather than issue a single-shot request, because it is always possible the mappings may change during the broadcast. ProgrammeDetector componentNeeded by recordForMe, this component examines the programme junction events it is sent, and determines when to start and stop recording. It looks up the channel name it has been given using the channel name lookup service, so it can know which events to watch for:
def main(self): It can then go into a loop, waiting until it sees an event for the start of a programme with the right programme name and service ID. It can then send on a message to instruct the recorder to start:
while 1: recording=False while not recording: if self.dataReady("inbox"): newNowEvent = self.recv("inbox") if newNowEvent['service'] == service_id: recording = newNowEvent['name'].lower().strip() == self.programme_name else: self.pause() yield 1 # start recording service_id = newNowEvent['service'] self.send("START", "outbox") The component then waits for an event signalling the start of another programme with the same service ID but a different programme name. It can then signal the recorder to stop:
while recording: if self.dataReady("inbox"): newNowEvent = self.recv("inbox") if newNowEvent['service'] == service_id: recording = newNowEvent['name'].lower().strip() == self.programme_name else: self.pause() yield 1 # stop recording self.send("STOP", "outbox") ControllableRecorder componentNeeded by recordForMe, this component works out what packet IDs contain the audio and video data for the channel, then waits to be instructed to start or stop. It requests the audio and video packets from the demuxer whilst it is supposed to be recording. First it looks up the channel name it has been given using the channel name lookup service:
def main(self): ...Armed with the service ID, it can then look in the Program Association Table (PAT) to find the packet ID for the corresponding Program Map Table (PMT). The PMT lists what packet IDs contain the audio and video data. A ParseProgramAssociationTable component is fed the table from the "PSI_Tables" service, to parse it: (the service name is in self.fromPSI)
pat_parser = Pipeline( Subscribe(self.fromPSI, [PAT_PID]), ParseProgramAssociationTable() ).activate() fromPAT_linkage = self.link( (pat_parser,"outbox"),(self,"_fromPAT") )The parsed table is collected, then searched for the packet ID of the PMT for the given service ID:
# 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 pat_table = self.recv("_fromPAT") for transport_stream_id in pat_table['transport_streams']: ts_services = pat_table['transport_streams'[]transport_stream_id] if service_id in ts_services: PMT_PID = ts_services[service_id] breakIt then sets up another parser component to parse the right PMT:
pmt_parser = Pipeline( Subscribe(self.fromPSI, [PMT_PID]),The parsed table is collected, then searched for the packet IDs for the audio and video:
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 ControllableRecorder is now ready, so it links up to the "DEMUXER" service, and waits to receive the "START" order:
# get the demuxer service cat = CAT.getcat() service = cat.retrieveService(self.fromDemuxer) self.link((self,"_toDemuxer"),service) while 1: # now wait for the go signal recording = False while not recording: if self.dataReady("inbox"): recording = self.recv("inbox") == "START" else: self.pause() yield 1 To start recording, it sends a request to the demultiplexer, asking to be sent the audio and video packets for the service:
# request audio and video data self.send( ("ADD",[audio_pid,video_pid], (self,"_av_packets")), "_toDemuxer") And forwards them out of its "outbox" outbox until it receives the "STOP" command:
while recording: while self.dataReady("_av_packets"): packet = self.recv("_av_packets") self.send(packet,"outbox") while self.dataReady("inbox"): recording = not ( self.recv("inbox") == "STOP" ) if recording: self.pause() yield 1 Once it has been told to stop, it sends another request to the demultiplexer, asking to stop being sent audio and video packets:
self.send( ("REMOVE", [audio_pid,video_pid], (self,"_av_packets")), "_toDemuxer") This is quite a long example, but demonstrates that you can build quite complex systems, like a PVR in quite a modular fashion. |
Kamaelia
is an open source project originated from and guided by BBC
Research. For more information browse the site or get in
contact.
This is an ongoing community based development site. As a result the contents of this page is the opinions of the contributors of the pages involved not the organisations involved. Specificially, this page may contain personal views which are not the views of the BBC. (the site is powered by a wiki engine)
(C) Copyright 2008 Kamaelia Contributors, including the British Broadcasting Corporation, All Rights Reserved