QuickTime
Conferencing (QTC) is a new Apple technology that helps developers add
real-time sharing of sound, video, and data to their applications. This
overview suggests the different ways you can use QTC to help users collaborate.
The article describes the components that most developers will need to use to
take advantage of QTC and discusses Watcher and Caster, two QTC applications
that enable users to tune into network broadcasts and create broadcasts for
others to view.
Video telephones abound in science fiction movies. From Buck Rogers to Star
Trek, visions of the future show people communicating visually over long
distances. This futuristic technology is available to Macintosh developers and
users now. QuickTime Conferencing provides a platform for developers to easily
enable users to share sound, video, and data across a variety of networks.
QTC ships with selected Power Macintosh computers and with some hardware
bundles, and can be licensed by developers to ship with their applications.
Apple provides a basic videoconferencing application, Apple Media Conference
(AMC), and developers are encouraged to create QTC applications that
interoperate with AMC and add cool new collaborative features.
This article will give you background information on the QTC architecture, tell
you about the components that make up that architecture, and then describe in
detail the workings of two simple QTC applications, Watcher and Caster, that
enable the user to watch audio and video and to broadcast them onto a network.
This issue's CD contains the source code for these applications as well as the
QTC documentation and the extension and header files.
QuickTime Conferencing provides a platform for building Macintosh applications
that can send and receive audio, video, and data between computers connected on
a network. QTC supports basic two-way audio communication and a video
"telephone" type of connection, and it supports a wide variety of other models
as well. One of the goals of QTC is to provide developers with a set of tools
that make it easy to add real-time media sharing across a number of different
kinds of networks.
This opens up the possibility of adding sound and video to multiuser
applications where it would have been prohibitively difficult before -- and
these don't have to be conventional telephony-style applications. Imagine a
flight simulator that allows you to talk with your fellow squadron members, or
a groupware document-markup application that lets your fellow editors see your
expression upon examining the latest changes. Picture a regional educational
system that enables dozens of students to tune into an 8 A.M. lecture from
their dorm rooms across campus or across the state. This isn't the stuff of
science fiction anymore.
QTC uses many of the services provided by QuickTime itself and shares an
architectural basis in the Component Manager. QTC takes advantage of the Image
Compression Manager for video compression and decompression, the sequence
grabber components for capturing media, and the Movie Toolbox for recording
movies to disk. When new features and improvements are added to QuickTime, they
often can be used by QTC immediately. For example, components created for video
or sound compression in QuickTime are automatically available to QTC.
QTC's basic metaphor for real-time media connections is that of a conference.
Conferences are quite flexible and can be configured in a variety of ways. They
can have one, a few, or many members, connected symmetrically or
asymmetrically. As illustrated in Figure 1, connections can take one of three
forms: point to point, for two-way conferences; multipoint, for virtual
meetings and groupware applications; or broadcast, for transmitting from one
member to many others.
Figure 1. The three types of conference connections
Members can send or receive sound, video, or data. Media types can be added,
removed, or changed during a conference. Members can join or leave a conference
at any time. Conferences can be merged, and data can be sent to one or all of
the conference members.
Depending on the application, you may want to give users a single configuration
-- say, a two-way audio and video connection -- or allow them to modify the
conference configuration themselves. QTC was designed to support a wide variety
of conference configurations and to leave it up to developers to decide which
features they need. Indeed, some applications may need to switch between
different configurations within a single conference. The applications described
later in this article each operate in a single configuration; one can broadcast
video and sound to an unlimited number of recipients and the other can tune
into one or more broadcast conferences.
QTC is network, protocol, and media independent. This means that applications
don't have to know the specifics of a particular network to set up a QTC
conference. QTC 1.0.2 ships with support for TCP/IP and AppleTalk networks;
third parties and Apple are working on adding new networks like ISDN,
isoEthernet, and ATM to the list. QTC 1.0.2 supports a new media-oriented
network protocol, called MovieTalk, but can also support other media protocols
such as the ITU H.320 standard and the emerging standards used on the Internet
Multicast Backbone (MBONE).
The media that flows between conference members is organized into one or more
streams of a particular media type. QTC 1.0.2 supports sound and video streams,
which can be compressed with any sound or video compressor. Future versions of
QTC will be able to support other media types, such as music and text, to
parallel the different track types that can be stored in a QuickTime movie.
QTC provides some of the basic user interface elements called for in a
conferencing application. For example, each member of a conference can be
represented on the screen with a stream controller, in much the same way that a
QuickTime movie controller provides a control representation for a QuickTime
movie. In fact, the stream controller and the movie controller share a similar
user interface, so that a user who has some experience with one can apply that
knowledge to the other.
QTC also provides a standard user interface enabling users to choose who to
call and include in a QTC conference, in the form of browser components.
Browsers work a bit like the Standard File Package that allows users to open
and save files: they provide a standard interface for choosing fellow users or
searching through PowerTalk catalogs to find other conference members and place
calls to them.
QTC, like much of QuickTime, is built of Component Manager components. Apple
provides a basic suite of components that enable the user to share data and
send and receive compressed video and audio on a few different networks. Before
we dive into our example applications, let's go over some of the component
types that make up the QTC component suite.
There are three main types of QTC components that most developers will need to
know about to add QTC support to their applications: the conference component,
the stream controller component, and the browser component. I'll describe these
in some detail. Developers who want to do fancier things will probably need to
know about some of the other components; the key ones are briefly described
later.
Because of the modular architecture of QTC, developers can add, extend, or
replace features and components. For example, a developer who wants to add
support for a new network multimedia protocol can create a new transport
component and register it with the Component Manager. Applications can then
find that component and specify its use in a conference. Developers who want to
improve on the QTC stream controller can capture the standard controller,
delegate many of the functions, and replace the ones of interest.
The conference component is the key player in a QTC conference. It acts as a
central hub and does the bulk of the work required to orchestrate the comings
and goings of the conference. It's responsible for listening in on the various
networks, placing and answering calls, managing and merging multiple
conferences, and more. The conference component can also provide some
higher-level functionality, such as setting up media capture, handling user
events, and even creating and managing conference windows.
Applications create a conference component instance and let the conference
component do much of the work needed to create, manage, and end conferences.
Applications can then tell the conference component to listen on the networks
for incoming calls or to place a call to another member.
Conference components create conference events when they need to express some
change in a conference to the application. For example, when an incoming call
is made to a conference, the conference component will generate an event of
type mtIncomingCallEvent to notify the application of the call. Applications
call the component routine MTConferenceGetNextEvent periodically to get the
events from the conference component, much as applications call the system
routine WaitNextEvent to get user and system events from the Event Manager.
In response to these conference events, applications work with the conference
component to respond appropriately -- for example, creating a window to display
a new conference member or send messages to other conference members. Details
of working with the conference component will be discussed later when we look
at our sample applications, Watcher and Caster.
Stream controllers are responsible for handling the default user interface for
controlling QTC media streams as well as managing their display on the screen
and through the speaker. The conference component is responsible for creating
and managing stream controller components. Applications are passed references
to the stream controllers by the conference component so that they can keep
track of where and how the media is being displayed.
The standard stream controller looks quite a bit like the standard QuickTime
movie controller, with buttons to control the flow of media, resize the visual
portion of the stream, and adjust the sound levels. The stream controller adds
some utility buttons that the movie controller doesn't have: a snapshot button
for capturing the current image displayed in the controller and a record button
that provides a standard way for a user to record the media in a stream
controller. (The conference component or the application is responsible for
actually handling the snapshots or recorded movies after the controller has
initiated them.)
Controllers associated with the sending side of a media stream (known as source
controllers) have a slightly different appearance and behavior from those
associated with the receiving side (known as sink controllers), as shown in
Figure 2. The source controller may have a microphone "gain" button that's
animated to indicate the level of the audio being sent across the connection.
Users who click this button can adjust the volume of the sound being sent
across the connection. On the receiving end, the sink controller may display a
volume control button that behaves like the speaker button on the standard
movie controller, allowing the user to adjust the volume of the incoming
stream.
Figure 2. Source and sink controller user interfaces
To place a call or add another member to a conference, the user needs to
specify the other member to call. Browser components provide a simple way for
users to browse the network and identify other members. Browser components come
in two flavors: network-specific browsers and the PowerTalk browser. The
PowerTalk browser and browsers specific to TCP/IP and AppleTalk are shown in
Figure 3.
Figure 3. Browsers
For each different network type -- such as TCP/IP or AppleTalk -- unique
browser components are provided that allow the user to specify a
network-specific address. For example, as shown in Figure 3, the AppleTalk
browser presents the user with a Chooser-style interface whereby the user can
choose the zone and then the registered name within that zone on an AppleTalk
network, similar to using the Chooser to pick a LaserWriter on an AppleTalk
network. The TCP/IP browser provides a simple type-in interface that can accept
TCP/IP addresses in numerical or text form.
The PowerTalk browser, on the other hand, is considered a generic or universal
browser, not tied to a particular network or addressing scheme. Users who have
PowerTalk installed can take advantage of the various PowerTalk catalogs and
business cards; these provide an integrated way for users to organize and find
other QTC users in the same way that they access electronic mail addresses via
PowerTalk. The PowerTalk browser allows the user to choose a business card from
a PowerTalk catalog that contains a QTC entry (provided by the QTC PowerTalk
Template). This works for local user catalogs and catalogs provided by
PowerShare servers, as well as the generic AppleTalk network catalog, which
allows the user to look out onto the network and into AppleTalk zones for other
users. Users can edit their personal catalogs from within the Finder,
consistent with the standard PowerTalk human interface.
Digital video and sound can generate a great deal of data, even when
compressed. Hard disk space is getting to be quite cheap, but network bandwidth
is still an expensive and shared commodity. To keep your fellow users and
network administrators happy, we developed multicast extensions to AppleTalk
that allow a single copy of QuickTime Conferencing media sent out onto a
network to be received and displayed by any number of users.
AppleTalk Multicast consists of a special packet format and a routing protocol
that makes efficient use of the network bandwidth. On a single network segment,
AppleTalk Multicast uses multicast packets that can be received by anyone on
that local network. On an AppleTalk internet, multicast-aware routers
communicate with each other with a new protocol called SMRP, the Simple
Multicast Routing Protocol, as shown in Figure 4. The routers deliver copies of
the media data only to other networks in which there's a user who wants to
receive that data. Networks with no users interested in the broadcast aren't
burdened with the network usage.
Apple has licensed AppleTalk Multicast and the SMRP protocol to Cisco Systems,
Inc. Cisco's router software as of version 11.0 supports this multimedia
protocol.
Figure 4. AppleTalk Multicast routing
QTC defines and uses many other kinds of components besides the three just
mentioned. Several of these component types may be of interest to developers
who want to add support for new networks or new media protocols; others may be
of use to developers who want to have more control over their conferences. Some
of these are listed here.
- Stream director components are responsible for managing the media streams
that flow between conference members. Stream directors are of two types: source
stream directors and sink stream directors. Source stream directors work with
media sources, such as QuickTime sequence grabbers, to capture audio and video
data to be sent across the network. Sink stream directors are responsible for
setting up and displaying incoming media data: video to the screen and sound to
the speaker. Conference components and controller components handle most of the
management and control of stream directors.
- Transport components are responsible for implementing the network
protocol that communicates media data, formats, and control information.
MovieTalk, the default QTC protocol, is implemented as a transport component.
Apple's H.320/ISDN conferencing card adds another transport type that supports
the ITU H.320 video conferencing standard. Developers who want to support new
media protocols can create new transport components to translate the control
messages from a conference into messages appropriate for the new protocol and
vice versa.
- Network components contain code specific to a given network type. QTC
1.0.2 provides network components for AppleTalk and TCP/IP. Future versions of
QTC will provide direct OpenTransport network interfaces as well as others.
Network components can provide access to multicast services on some shared
networks so that media data can be sent to multiple recipients without having
to send out multiple copies of that data. (See "About AppleTalk Multicast" for
a discussion of one such multicast service.) The conference component
automatically takes advantage of multicast network services when they're
available.
- Recorder components attach to stream directors and provide a mechanism
to record to disk the media sent or received within a conference. Apple
provides a recorder component that records media into QuickTime movies and can
attach to multiple members via stream directors to create movies of entire
conferences at once.
Several other components are used within QTC,
including player components, flow control components, and others of interest to
developers who want to extend QTC to support new networks, protocols, and
media. Figure 5 shows how a number of QTC components typically work together
within the all-encompassing conference component. For information on all of the
components that make up QTC, check out the QTC documentation on this issue's
CD.
Figure 5. How QTC components work together within the conference component
Probably the best way to show how to use QTC in an application is with some
examples, so we've created Watcher and Caster. Watcher lets the user tune into
broadcasts on AppleTalk networks, while Caster enables the user to create
broadcasts that can be watched by others on the AppleTalk network. Watcher and
Caster are compatible with Apple Media Conference (AMC), the QTC application
that Apple ships with selected CPUs and product packages, so you can use
Watcher to watch a broadcast that's being sent by AMC or Caster, and you can
use Caster to create broadcasts that can be received by Watcher and AMC.
Note that in several places in Watcher and Caster, we do some work manually
that otherwise could be done automatically by the conference component. We do
this extra work to demonstrate how you can customize an application if the
behavior that you want is different from the default behavior offered by the
conference component.
Watcher is a relatively simple Macintosh application. After setting up the
application environment, Watcher sets up the conference component that will
place calls and manage the incoming media. Then, within the event loop, the
application checks for user and system events and also checks the conference
component for conference events, which indicate changes in the conference state
and may require responses from the application.
The overall flow of Watcher or any QTC application that uses the conference
component is as follows:
QTCApp()
{
SetupApplication();
SetupConferenceComponent();
StartListening();
do {
ProcessUserEvents();
ProcessConferenceEvents();
} while (!gQuit);
CleanUpConferenceComponent();
CleanUpApplication();
ExitToShell();
}
Below,
I'll go into more detail about the three major application responsibilities --
setting up the conference component, handling conference events, and cleaning
up at the end of the conference -- showing the core routines that deal directly
with the conference component. Check out the full source code to see them in
the context of the entire application.
Listing 1 shows how the conference component is created and initially
configured. The Component Manager call OpenDefaultComponent is used to create
and open an instance of the conference component; then the conference component
mode is set to indicate that the conference will be used to receive media.
Finally, the component is told what networks to prepare for connections on --
AppleTalk in this case -- and how to identify itself on that network.
MTConferenceListen (as well as MTBrowserBrowse, a call we'll encounter a little
later) uses a C string of type MTCString to describe the network and transport
configurations. In Listing 1, the string "mtlkatlk\tNoIncomingCalls\x0D"
indicates that the conference component should listen for calls that have a
transport subtype of 'mtlk' (the component subtype for the MovieTalk transport
component) and a network subtype of 'atlk' (the subtype for AppleTalk
networks). The "\t" delimits the subtypes from the network-specific
configuration data that follows. For AppleTalk networks, this is the Name
Binding Protocol (NBP) type "No Incoming Calls." Finally, the configuration is
terminated with a carriage return ("\x0D"). You can string together multiple
configuration strings (each terminated with a carriage return) to listen in on
multiple networks for calls. Check out the full documentation for a more
complete explanation of the configuration strings.
Listing 1. CreateWatchConference
ComponentResult CreateWatchConference(MTCString63 userName)
{
ComponentResult err;
/* Create a conference record. */
err = NewConference(&gConference);
if (err == noErr) {
gConference->confComponent
= OpenDefaultComponent(kMTConferenceType,
kMTMovieTalkSubType);
if (gConference->confComponent) {
/* Tell the conference component that we only want to */
/* receive media, not send. */
err = MTConferenceSetMode(gConference->confComponent,
mtReceiveMediaModeMask);
/* Tell the conference component to prepare to use
AppleTalk.
The funky C string tells the conference component:
mtlk = use the MovieTalk transport component
atlk = use the AppleTalk network component
NoIncomingCalls = the AppleTalk-specific NBP type
that's used for listening;
i.e., there will be no incoming calls
*/
if (err == noErr)
err = MTConferenceListen(gConference->confComponent,
userName /* User name */,
userName /* Service name */,
(MTCString)"mtlkatlk\tNoIncomingCalls\x0D");
}
else
err = couldntGetRequiredComponent;
}
return err;
}
Now that the conference is set up, we can place a "call" out onto the network
to the broadcaster that the user wants to watch. We'll use the AppleTalk
browser component to pick a registered broadcaster.
The BrowseName routine (Listing 2) opens the browser component and uses the
MTBrowserBrowse component call to specify which kind of network entity to look
for. In this case it's a MovieTalk entity registered on an AppleTalk network
with the NBP type of "Multicaster"; this type identifies broadcasts from Caster
and AMC. MTBrowserBrowse then presents users with the browser dialog, where
they can "surf" the network and find the appropriate broadcaster. Some browsers
(like the PowerTalk browser) can return multiple names in an MTNameList. We're
only interested in the one AppleTalk broadcast picked by the user, so we pick
off the first MTName from the MTNameList.
Listing 2. BrowseName
ComponentResult BrowseName(MTNamePtr name)
{
MTNameListPtr allNames = 0;
ComponentResult err;
MTBrowserComponent browser = nil;
browser = OpenDefaultComponent(kMTBrowserType,
kMTAppleTalkSubType);
if (browser) {
err = MTBrowserBrowse(browser, 0, nil,
(MTCString)"mtlkatlk\tMulticaster\x0D", 0, &allNames);
CloseComponent(browser);
}
else
err = couldntGetRequiredComponent;
if ((allNames != 0) && (err == noErr)) {
/* Copy the first name record; that's all we're interested */
/* in. */
*name = allNames->list[0];
/* Dispose of the list of names. */
DisposePtr((Ptr)allNames);
}
return err;
}
CallMember (Listing 3) is the code needed to tell the conference component to
place a call to the broadcaster. The calling routine passes in the MTName
(obtained from BrowseName) and a pointer to the window in which the broadcast
is to appear (and that window's size). The resize parameter will be used later
to determine whether to resize the window automatically to the dimensions of
the video being broadcast. CallMember returns a pointer to a new MemberRecord
data structure, where the information about each broadcast-watching window is
kept. The important conference component call here is MTConferenceCall, which
is passed a reference to the conference component, an arbitrary name for the
conference, and the MTName describing the party whose broadcast we want to
watch.
Note that the conference component manages each independent connection to a
broadcaster as a unique conference. That's just fine for our application, since
the broadcast windows are really independent. In multiparty connections,
however, conferences can be joined and then individual members can belong to
the same conference. In that case the conference name parameter in
MTConferenceCall ("Watcher" in Listing 3) may have more meaning and may be used
to distinguish independent conferences. In our case, we give them all the same
name.
Listing 3. CallMember
ComponentResult CallMember(MTName* name, WindowPtr wind, Rect* box,
Boolean resize, MemberRecord** member)
{
MemberRecord* mr;
ComponentResult err;
/* Create a new member record. */
err = NewMember(&mr);
if (err == noErr) {
mr->member = MTConferenceCall(gConference->confComponent,
(MTCString)"Watcher", name);
mr->box = *box;
mr->window = wind;
mr->resize = resize;
if (member)
*member = mr;
}
return err;
}
Now that the conference call has been placed, we need to check the conference
component periodically to find out about changes in the conference. Listing 4
shows the routine CheckConferenceEvents, which is intended to be called within
the main event loop of the application. Each time through the loop, we call
MTConferenceGetNextEvent. Most of the time this will return false, indicating
that there are no new events. When some state in the conference has changed, it
will return true, and we should then parse the event (with
HandleConferenceEvent) to see what the correct response is.
Listing 4. CheckConferenceEvents
ComponentResult CheckConferenceEvents(void)
{
MTConferenceEvent confEvent;
ComponentResult err;
if (MTConferenceGetNextEvent(gConference->confComponent,
&confEvent))
err = HandleConferenceEvent(&confEvent);
return err;
}
The
MTConferenceEvent data structure, also known as an event record, has several
fields that we'll use in the following listings. The what field indicates the
type of event; depending on this, HandleConferenceEvent (Listing 5) switches to
the individual subroutines corresponding to each event. The surprise field, if
not set to 0, contains a handle to data that's associated with the event and
needs to be disposed of after use. The other fields, who, err, and bonus,
contain references to the members, error codes, and event-specific data,
respectively. See the documentation for details on the meanings of these fields
for all event types.
Listing 5. HandleConferenceEvent
ComponentResult HandleConferenceEvent(MTConferenceEventPtr confEvent)
{
ComponentResult err = noErr;
/* Like a user event handler, we switch on the different
conference events. */
switch (confEvent->what) {
case mtConferenceReadyEvent:
err = DoConfReady(confEvent);
break;
case mtMemberReadyEvent:
err = DoMemberReady(confEvent);
break;
case mtMemberTerminatedEvent:
err = DoMemberTerminated(confEvent);
break;
case mtMemberJoiningEvent:
err = DoMemberJoining(confEvent);
break;
case mtPhoneRingingEvent:
err = DoPhoneRinging(confEvent);
break;
case mtRefusedEvent:
case mtFailedEvent:
err = confEvent->err;
break;
default: /* Ignore all others. */
break;
}
/* If there's data associated with this event, free it. */
if (confEvent->surprise)
DisposeHandle(confEvent->surprise);
return err;
}
After
a call has been placed and a connection has been established with the remote
side, an event of type mtMemberJoiningEvent is returned by the conference
component. Upon receiving this event our application calls DoMemberJoining
(Listing 6) and simply makes a record of this new member and adds it to our
list of members. The conference component will continue to establish the
connection and will notify us further when the connection has been completely
brought up.
Listing 6. DoMemberJoining
struct MemberRecord {
MTControllerComponent controller;
MTDirectorComponent director;
MTConferenceMember member;
WindowPtr window;
Boolean resize;
Rect box;
MemberRecord* next;
};
...
ComponentResult DoMemberJoining(MTConferenceEventPtr confEvent)
{
MemberRecord* currMember;
ComponentResult err = noErr;
err = NewMember(&currMember);
if (err != noErr) {
currMember->member = confEvent->who;
AddMember(gConference, currMember);
}
return err;
}
Once
the connection has been fully established, the conference component sends us an
event of type mtMemberReadyEvent. Now we have a little more work to do. In this
case, the application needs to create a controller and place that controller
into a window for incoming media to be displayed. The conference component can
do much of this work for you, including creating a controller (and its
associated stream director) as well as creating a window and even handling user
events for that window, with the MTConferenceNewPreparedController call. For
many applications this method is perfectly adequate, but if you need more
control over event handling and window management in your application, you'll
want to do this work manually, as we do in Watcher and Caster. Use of
MTConferenceNewPreparedController is demonstrated in the SeeWorld sample
applications included on this issue's CD; check out the Rogues and Guardian
examples in particular.
DoMemberReady (Listing 7) first checks to see if we can expect media to be sent
by the new member. (If the member isn't sending media, there's no point in
setting up a window.) If the member is sending media, we create a controller
component and a stream director component, which are responsible for displaying
the media data. After this, we call MTControllerNewAttachedController to
connect the controller to the stream director and point it at a window for
display. We then do one more thing to the controller before activating it in
the conference: we set an action filter for it. The action filter is a callback
routine that the controller calls whenever any important action happens within
the controller. In our application, the only action that we care about is the
resizing of the media data so that we can resize the window. The action filter
routine is shown in Listing 8.
Listing 7. DoMemberReady
ComponentResult DoMemberReady(MTConferenceEventPtr confEvent)
{
ComponentResult err = noErr;
MemberRecord* currMember;
Point where = {0, 0};
Boolean aTrue = true;
if (confEvent->bonus & mtReceiveMediaModeMask) {
currMember = FindMember(gConference, confEvent->who);
if (currMember == nil)
return noErr;
currMember->controller =
OpenDefaultComponent(kMTControllerType,
kMTMovieTalkSubType);
if (currMember->controller == 0)
err = couldntGetRequiredComponent;
if (err == noErr) {
currMember->director = OpenDefaultComponent(
kMTSinkStreamDirectorType, kMTPlayerType);
if (currMember->director == 0)
err = couldntGetRequiredComponent;
}
if (err == noErr)
err = MTControllerNewAttachedController(
currMember->controller, currMember->director,
currMember->window, where);
if (err == noErr)
err = MTControllerSetActionFilter(currMember->controller,
actionFilterUPP, (long)currMember);
if (err == noErr)
err = MTConferenceActivateMember(gConference->confComponent,
confEvent->who, currMember->controller);
if (err == noErr)
err = MTControllerDoAction(currMember->controller,
mtControllerActionPlay, &aTrue);
}
return err;
}
Listing 8. MyControllerActionFilter
pascal Boolean MyControllerActionFilter(MTControllerComponent mtc,
MTControllerActionType action,
void* params, long refCon)
{
void* unused1 = params;
long unused2 = refCon;
RgnHandle controllerRgn;
Boolean handled = false;
Rect box;
WindowPtr controllerWindow =
(WindowPtr)MTControllerGetControllerPort(mtc);
switch (action) {
case mtControllerActionControllerSizeChanged:
/* Find out how big the controller is. */
controllerRgn = MTControllerGetWindowRgn(mtc,
controllerWindow);
/* Resize the window accordingly. */
if (controllerRgn != nil) {
box = (**controllerRgn).rgnBBox;
DisposeRgn(controllerRgn);
SizeWindow(controllerWindow, box.right, box.bottom,
true);
}
break;
default:
break;
}
return handled;
}
Finally,
DoMemberReady calls MTConferenceActivateMember to activate the member, and we
pass MTConferenceActivateMember the newly created controller. Before exiting,
we call MTControllerDoAction to tell the controller component to begin playing
the incoming media as soon as it begins. (Controllers are by default in a
paused state when they're created.)
When the user has decided to close down the reception of the broadcast (say, by
closing a broadcast window), the application calls CloseWatch (Listing 9).
CloseWatch will find the member record corresponding to the conference member
and obtain the conference token associated with that member. (Remember, each
member is part of a unique conference, so the member has both a conference
token and a unique ConferenceMember identifier.) Then we begin to terminate the
conference by calling MTConferenceTerminate.
Listing 9. CloseWatch
ComponentResult CloseWatch(WindowPtr window)
{
ComponentResult err = noErr;
MTConferenceToken theConference;
MemberRecord* theMember;
theMember = FindMemberWindow(gConference, window);
if (theMember == nil)
err = paramErr;
if (err == noErr) {
theConference = MTConferenceGetMemberConference(
gConference->confComponent,
theMember->member);
err = MTConferenceTerminate(gConference->confComponent,
theConference);
}
return err;
}
The
conference isn't completely terminated until we receive an event of type
mtMemberTerminatedEvent, which is handled by DoMemberTerminated (Listing 10).
DoMemberTerminated is called when the conference connection for this member has
been completely terminated, either by an MTConferenceTerminate call or by the
remote side closing down. In response, we'll close down the controller and
stream director components and the associated window, then free up our
application's MemberRecord for this member.
Listing 10. DoMemberTerminated
ComponentResult DoMemberTerminated(MTConferenceEventPtr confEvent)
{
MemberRecord* member;
ComponentResult err;
member = FindMember(gConference, confEvent->who);
if (member == nil)
return noErr;
RemoveMember(gConference, member);
if (member->controller)
CloseComponent(member->controller);
if (member->director)
CloseComponent(member->director);
if (member->window)
CloseWindow(member->window);
err = DisposeMemberRecord(member);
return err;
}
That's
it for the key QTC routines in Watcher. Check out the source code on the CD to
see the entire package come together.
Caster, the broadcasting side of this networked multimedia system, is similar
to Watcher in many ways. It uses a conference component (see Figure 6) and
processes conference events, but it handles the other side of the conference
establishment: setting up and transmitting media and accepting incoming calls.
In some ways, Caster is simpler: since it broadcasts to anybody who wants to
tune in, it doesn't need to keep track of each member individually.
Figure 6. A QTC broadcaster and two watchers
Probably the trickiest part of Caster is the code that sets up the sequence
grabber to capture video and sound. The call MTConferenceNewPreparedController
from the conference component could be used to set up the sequence grabber (as
well as the controller and stream director) in many cases, but as mentioned
earlier for Watcher, this call won't be adequate if you need more control.
In the SetupSequenceGrabber routine (Listing 11), we first create the sequence
grabber component by calling OpenDefaultComponent. Once the component is
initialized with SGInitialize, we create the individual sound and video
channels. We can use other calls in the sequence grabber component API to
adjust settings, like frame rate and compressor type. We also need to call
SGSetChannelUsage to tell the controller that the channels can be used for
preview and record and that they will play through during recording
(seqGrabPreview + seqGrabRecord + seqGrabPlayDuringRecord).
Listing 11. SetupSequenceGrabber
ComponentResult SetupSequenceGrabber(
SeqGrabComponent* sg, SGChannel* soundChannel,
SGChannel* videoChannel)
{
ComponentResult err = noErr;
SeqGrabComponent grabber = nil;
*soundChannel = nil;
*videoChannel = nil;
grabber = OpenDefaultComponent(SeqGrabComponentType, 0);
if (grabber == nil)
err = couldntGetRequiredComponent;
else {
err = SGInitialize(grabber);
if (err == noErr) {
err = SGNewChannel(grabber, SoundMediaType, soundChannel);
if (err == noErr)
SGSetChannelUsage(*soundChannel,
seqGrabPreview + seqGrabRecord);
err = SGNewChannel(grabber, VideoMediaType, videoChannel);
if (err == noErr) {
SGSetFrameRate(*videoChannel, 0);
/* 'rpza' is the Apple Video Compressor. */
SGSetVideoCompressorType(*videoChannel, 'rpza');
SGSetChannelUsage(*videoChannel,
seqGrabPreview + seqGrabRecord
+ seqGrabPlayDuringRecord);
}
/* Reset in case we had a problem opening a channel */
/* (e.g., there was no digitizer). */
err = noErr;
}
}
if (err != noErr) {
if (grabber)
CloseComponent(grabber);
grabber = nil;
}
*sg = grabber;
return err;
}
Now that we have the sequence grabber created as a source for captured data, we
need to hook it up to the stream director and controller and create a pipeline
for the media, which will eventually be fed into the conference component and
out onto the network. OpenCast (Listing 12) takes a sequence grabber and a
window to display it in, creates a source stream director and controller, and
configures them.
Listing 12. OpenCast
typedef struct {
WindowPtr window;
SeqGrabComponent sg;
MTConferenceComponent confComponent;
MTControllerComponent controller;
MTDirectorComponent director;
Boolean casting;
MTConferenceToken conference;
} CastRecord;
...
ComponentResult OpenCast(WindowPtr window, SeqGrabComponent sg, CastRecord** cr)
{
ComponentResult err = noErr;
CastRecord* newRecord = nil;
Point origin = {0,0};
/* Specify the default window bounds for a 160-by-120 video window; add 16 to the height
to make space for the controller. */
Rect bounds = {0, 0, 120 + 16, 160};
Boolean aFalse = false;
newRecord = (CastRecord*)NewPtrClear(sizeof(CastRecord));
if (newRecord == nil)
err = MemError();
if (err == noErr) {
newRecord->window = window;
newRecord->sg = sg;
newRecord->director = OpenDefaultComponent(kMTSourceStreamDirectorType, kMTGrabberSubType);
if (newRecord->director == nil)
err = couldntGetRequiredComponent;
}
if (err == noErr) {
newRecord->controller = OpenDefaultComponent(kMTControllerType, kMTMovieTalkSubType);
if (newRecord->controller == nil)
err = couldntGetRequiredComponent;
}
if (err == noErr)
err = MTControllerSetActionFilter(newRecord->controller, actionFilterUPP, 0);
if (err == noErr)
err = MTDirectorSetMediaComponent(newRecord->director, sg);
if (err == noErr)
err = MTControllerNewAttachedController(newRecord->controller, newRecord->director, window,
origin);
if (err == noErr)
err = MTControllerDoAction(newRecord->controller, mtControllerActionSetShowSnapshot, &aFalse);
if (err == noErr)
err = MTControllerSetControllerBoundsRect(newRecord->controller, &bounds);
if (err == noErr)
*cr = newRecord;
else
CloseCast(newRecord);
return err;
}
After
the source stream director and controller are created, we attach a controller
action filter routine (as we did before for Watcher) and connect the sequence
grabber to the stream director with the MTDirectorSetMediaComponent call. The
value of the source stream director subtype is the same as the value of the
sequence grabber type, indicating that this source stream director has a
sequence grabber as its source. We then call MTControllerNewAttachedController
to attach the controller to the stream director; MTControllerDoAction with
mtControllerActionSetShowSnapshot, passing in false to hide the snapshot button
(not the default behavior); and finally MTControllerSetControllerBoundsRect to
give the controller an initial bounds size.
Now that we're ready to start broadcasting, we'll create the conference
component and have it start listening for incoming calls from watchers, as
shown in Listing 13. MTConferenceSetMode indicates to the controller that we'll
want to send media (which we didn't want to do with Watcher) and that we expect
to share a single director/controller source with multiple members of a
conference. We won't actually attach the controller/director/sequence grabber
chain to the conference component until somebody calls in.
Listing 13. StartCasting
ComponentResult StartCasting(CastRecord* cr, Str63 name)
{
MTCString63 cName;
ComponentResult err = noErr;
PToCString(name, cName);
cr->confComponent =
OpenDefaultComponent(kMTConferenceType, kMTMovieTalkSubType);
if (cr->confComponent == nil)
err = couldntGetRequiredComponent;
if (err == noErr)
err = MTConferenceSetMode(cr->confComponent,
mtSendMediaModeMask + mtShareableModeMask);
if (err == noErr)
err = MTConferenceListen(cr->confComponent, cName, cName,
(MTCString)"mtlkatlk\tMulticaster\x0D");
if (err == noErr)
cr->casting = true;
return err;
}
Finally,
we begin listening with the call to MTConferenceListen, passing it the C string
indicating the transport, network, and configuration information. In this case
the transport type is 'mtlk' for the MovieTalk protocol transport component,
the network type is 'atlk' for AppleTalk, and the configuration string is
"Multicaster"; the latter will be used by AppleTalk as an NBP type. This is the
AppleTalk NBP type that the browser in Watcher looked for while browsing the
network. (This is also the type that AMC uses, so we'll be able to watch Caster
broadcasts with it, too.)
Once the conference component has been set up, Caster periodically checks it
for conference events, just as Watcher does. Some of the behavior in response
to these events is a little different, mainly because Caster is receiving
incoming calls and sending media. Listing 14 shows the routines that get called
in response to the following conference events: mtIncomingCallEvent,
mtConferenceReadyEvent, mtMemberReadyEvent, and mtConferenceTerminatedEvent.
Listing 14. Routines for responding to conference events
ComponentResult DoIncomingCall(CastRecord* cr,
MTConferenceEventPtr confEvent)
{
return MTConferenceReply(cr->confComponent, confEvent->who, 0);
}
ComponentResult DoConferenceReady(CastRecord* cr,
MTConferenceEventPtr confEvent)
{
ComponentResult err = noErr;
if (cr->conference == 0) {
cr->conference = confEvent->who;
err = MTConferenceActivateConference(cr->confComponent,
cr->conference, cr->controller);
}
else
err = MTConferenceMerge(cr->confComponent, cr->conference,
confEvent->who);
return err;
}
ComponentResult DoMemberReady(CastRecord* cr,
MTConferenceEventPtr confEvent)
{
ComponentResult err = noErr;
err = MTConferenceActivateMember(cr->confComponent,
confEvent->who, 0);
if (err == noErr)
err = MTConferenceDetachMember(cr->confComponent,
confEvent->who);
return err;
}
ComponentResult DoConferenceTerminated(CastRecord* cr,
MTConferenceEventPtr confEvent)
{
ComponentResult err = noErr;
if (cr->conference == confEvent->who) {
cr->conference = 0;
MTControllerDoAction(cr->controller, mtControllerActionPlay,
&aTrue);
}
return err;
}
In
response to an mtIncomingCallEvent, the DoIncomingCall routine simply invokes
the conference component's MTConferenceReply function to essentially answer the
call immediately. A more complex version of this routine might check the
caller's identity to determine whether the caller has permission to watch the
broadcast. Caster will take all callers and reply immediately.
Upon receipt of the mtConferenceReadyEvent, passed when the conference has been
fully established, we'll take one of two courses of action:
- If this is the first incoming caller, and therefore the first conference,
we'll save the conference token (in the conference event's who field) and
activate the conference with the MTConferenceActivateConference function. This
is where we connect up the controller/stream director/sequence grabber
configuration by passing in a reference to the source controller.
- If this is the second or later watcher tuning in, this watcher will
join as a new member in a new conference. We'll call MTConferenceMerge to merge
this new conference with the original conference so that the new member is sent
the media.
Now that the conference is set up, we should expect to receive
an event of type mtMemberReadyEvent. Here we simply activate the member to
start receiving the broadcast. Then we call a special function designed to help
us take advantage of multicast network services if available,
MTConferenceDetachMember. This function will "detach" the member from a direct
point-to-point connection and will rely on multicast services to get the member
its data. In this case the receiving side and Caster can't send reliable
messages to each other, but for our application that's just fine; we'd rather
minimize the network traffic.
Finally, when a watcher disconnects, for whatever reason, we're notified with
an mtConferenceTerminatedEvent and call DoConferenceTerminated. If this is the
first conference, we forget about it by resetting our conference token to 0.
(We also get termination events for conferences that were merged, so we just
ignore those.) When the connection is torn down the media is stopped by the
stream director, so to continue the preview for the user we tell the controller
to start playing again with the MTControllerDoAction function.
Typically, developers want to enable users to change media settings of the
sequence grabber when it's connected to the other components and even when
we're sending to a conference. In Listing 15, we use the sequence grabber
SGSettingsDialog function to present users with a configuration dialog so that
they can change the video or audio settings. It's not really safe to talk to
the sequence grabber directly without warning the other parts of the connection
that the media formats will change.
Listing 15. CastChannelSettings
ComponentResult CastChannelSettings(CastRecord* cr,
SGChannel channel)
{
ComponentResult err = noErr;
err = MTControllerChangedStreams(cr->controller, false);
if (err == noErr) {
err = SGSettingsDialog(cr->sg, channel, 0, nil, 0, nil, nil);
MTControllerChangedStreams(cr->controller, true);
}
return err;
}
We
surround the call to SGSettingsDialog with calls to the controller function
MTControllerChangedStreams. The second parameter is a Boolean that indicates
whether we've finished changing the streams. Calling MTControllerChangedStreams
with this parameter set to false pauses the media in the connection and makes
it safe to change the setting. Then after the sequence grabber has been
adjusted, we call MTControllerChangedStreams again with this parameter set to
true to indicate that we're done. This in turn starts the process of
"renegotiating" the media formats across the connection safely.
There's a wealth of documentation available to help you add QTC support to your
new and existing applications.
Inside Macintosh: QuickTime Conferencing can be found on this issue's CD,
documenting the API for all of the QTC components as well as the MovieTalk
protocol. The rest of the QTC documentation, including more sample code, human
interface notes, and documentation on AppleTalk Multicast, can be found on the
Mac OS SDK edition of the Developer CD Series. To learn about the intricacies
of the sequence grabber and other media- and component-related topics, check
out Inside Macintosh: QuickTime and Inside Macintosh: QuickTime Components.
Come visit us on the World Wide Web at http://qtc.quicktime.apple.com/; you'll
find abundant QTC information there, including developer documentation and free
software. To share your ideas about uses for QTC, you can reach the QTC team at
movietalk@applelink.apple.com (AppleLink MOVIETALK). To get the licensing terms
for QTC, contact Apple's Software Licensing department at
sw.license@applelink.apple.com (AppleLink SW.LICENSE) or (512)919-2645, or
write to Apple Computer, Inc., 2420 Ridgepoint Drive, M/S 198-SWL, Austin, TX
78754.
I hope that I've been able to give you an idea of what QuickTime Conferencing
is all about and how to get started using this exciting new technology. No
longer just the stuff of science fiction, videophone and other multimedia
connections can be part of the Macintosh experience for everyone.
Thanks to our technical reviewers Eric Carlson, Brian Cox, Godfrey DiGiorgi,
Kevin Gong, Eric Hoffert, and Guy Riddle.
DEAN BLACKKETTER (dean@artemis.com) used to work for Apple in the Advanced
Technology Group. He now has a gig with Artemis Research working on "the next
big thing." He plays in San Francisco with his wife, their cat, and the scary
elf who lives on top of the fridge.