![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/all/themes/custom_front/images/you_are_here_red.gif)
![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/default/files/beta-site.gif)
|
Volume Number: 16 (2000)
Issue Number: 11
Column Tag: QuickTime Toolkit
Word is Out
by Tim Monroe
Using Text in QuickTime Movies
Introduction
When QuickTime was first introduced, it was able to handle two types of media data: video and sound. Curiously, the very next media type added to QuickTime (in version 1.5) was text, or the written word. Part of the motivation for adding text media was to provide the sort of "text below the picture" that you see in movie subtitles or television closed-captioning, as illustrated in Figure 1. Here, the text provides the words of a song, which can be useful to hearing-impaired or non-English speaking users. Similarly, the text might provide the dialogue of a play or a readable version of the narration. Of course, the text doesn't have to just mirror the voice part of an audio track; it can be any annotation that the movie creator deems useful for the viewer.
Figure 1. A movie containing a text track.
The text you see in Figure 1 is not part of the video track; rather, it is stored in a text track (whose associated media is of type TextMediaType). Typically the text track is situated below the video track (as in Figure 1), but in fact it can overlay part or all of the video track. In order for both the text and the overlain video to be visible, the background of the text track should be transparent or "keyed out"; the text is then called keyed text. Figure 2 shows some keyed text overlaying a video track. Keying can be computationally expensive, however, so keyed text is seen less often than below-the-video text.
Figure 2. A movie containing a keyed text track.
QuickTime provides the capability to search for a specific string of characters in a text track and to move the current movie time forward (or backward) to the next (or previous) occurrence of that string. In addition, the standard movie controller provides support for a special kind of text track called a chapter track. A chapter track is a text track that has been associated with some other track (often a video or sound track); when a movie contains a chapter track, the movie controller will build, display, and handle a pop-up menu that contains the text in the various samples in that track. The pop-up menu appears (space permitting) in the controller bar. The various parts of the associated track are called the track's chapters. When the user selects an item in the pop-up menu, the movie controller jumps to the start time of the selected chapter. Figure 3 shows our standard appearing-penguin movie with a chapter track that indicates the percentage of completion (both before and after the user clicks on the pop-up menu). Notice that we've had to hide the step buttons in the controller bar to make room for the chapter pop-up menu. Notice also that the text track itself is not visible.
Figure 3. A movie with a chapter track.
The QuickTime Player application, introduced with QuickTime 3.0, employs a slightly different user interface for accessing a movie's chapters. As you can see in Figure 4, a QuickTime Player movie window replaces the pop-up menu with a set of up- and down-arrow controls, which select the previous and next chapter.
Figure 4. The chapter controls in a QuickTime Player movie window.
QuickTime 3.0 also included a web browser plug-in that supports linked text. Linked text is contained in a hypertext reference track (usually shortened to HREF track), which is simply a text track that has a special name (to wit, "HREFTrack") and contains some media samples that pick out URL links. If a text sample contains text of the form <URL>, the QuickTime Plug-In will load the specified URL in the frame containing the movie when the user clicks in the movie box while that text sample is active. (Let's call this a clickable link.) If the text is if the form A<URL>, then the plug-in will load the specified URL automatically when that text sample becomes active. (Let's call this an automatic link.)
QuickTime 4 added one more text-handling tool, the ability to attach wired actions to data in a text track. A wired action is some action (such as setting a movie's volume or its current time) that is initiated by some particular event. The events that can trigger wired actions include both user events like moving or clicking the mouse and movie controller events like loading movies or processing idle events from the operating system. We'll investigate wired actions at length in a future article; for the moment, consider the movie shown in Figure 5. This movie contains only one track, a text track. The text track is configured so that clicking on the word "Apple" launches the user's default web browser and loads the URL http://www.apple.com; in addition, rolling the cursor over the word "CNN" loads the URL http://www.cnn.com/. (Let's call this wired text.)
Figure 1. A text track with wired actions.
In this article, we're going to take a look at the most basic ways of handling text in QuickTime movies. After we take a brief detour to upgrade the code that adjusts our Edit menu, we'll uncover some ways in which our existing sample applications can already interact with text. It turns out that these applications can do a surprising amount of work with text tracks; indeed, they can even create text tracks, in spite of the fact that they contain no text-specific code. So we'll spend a little bit of time to see how that's possible. Then we'll see how to create text tracks using the standard Movie Toolbox functions. We'll also learn how to search and edit text tracks. Toward the end of this article, we'll see how to create chapter tracks and HREF tracks. When all is said and done, we'll have at hand the essential tools that we need to create text tracks, keyed text, chapter tracks, and linked text. Figure 6 shows the Test menu of this month's sample application, named QTText.
Figure 6. The Test menu of QTText.
The Edit Menu Revisited
Let's begin by considering our code for enabling and disabling items in the Edit menu. (This might appear to have nothing at all to do with text handling, but it is actually fairly germane to this topic. Trust me.) Currently, when the user clicks in the menu bar to select one of our application's menus, our application framework calls the QTFrame_AdjustMenus function. (In our Macintosh framework, this happens in response to a mouseDown event in the inMenuBar window part; in our Windows framework, this happens when the MDI frame window receives the WM_INITMENU command.) Listing 1 shows the code in QTFrame_AdjustMenus that adjusts the Edit menu.
Listing 1: Adjusting the Edit menu (original version)
QTFrame_AdjustMenus #if TARGET_OS_MAC myMenu = GetMenuHandle(kEditMenuResID); #endif if (myMC != NULL) { long myFlags; MCGetControllerInfo(myMC, &myFlags); QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, myFlags & mcInfoUndoAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT, myFlags & mcInfoCutAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, myFlags & mcInfoCopyAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, myFlags & mcInfoPasteAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, myFlags & mcInfoClearAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, myFlags & mcInfoEditingEnabled ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, myFlags & mcInfoEditingEnabled ? kEnableMenuItem : kDisableMenuItem); } else { QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, kDisableMenuItem); }
There's nothing particularly complicated here: if there is no movie controller associated with the frontmost window or there is no frontmost window, then we disable all the items in the Edit menu (that's the "else" portion). Otherwise, we call the MCGetControllerInfo function to determine the current status of the movie controller and its associated movie. MCGetControllerInfo returns a set of flags that indicate which editing operations currently make sense for the specified movie controller and its movie. For instance, if there is some data available for pasting and editing is enabled, then the mcInfoPasteAvailable flag is set in the 32-bit long integer returned by MCGetControllerInfo. In this case, our application should enable the Paste menu item. Conversely, if either editing is disabled for the specified movie or there is nothing to paste, then that flag is clear. In that case, the Paste menu item should be disabled. We call the function QTFrame_SetMenuItemState to enable or disable the Paste menu item, like this:
QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, myFlags & mcInfoPasteAvailable ? kEnableMenuItem : kDisableMenuItem);
We've already considered QTFrame_SetMenuItemState in an earlier article (see "QuickTime 101" in MacTech, January 2000); it just calls the appropriate platform-specific function for enabling or disabling a menu item.
Emulating QuickTime Player
So far, so good. But there is a very important capability that we still need to add to our sample applications. If we launch the QuickTime Player application, open a movie, make a selection, and then hold down the Option key (or, on Windows, both the Ctrl and Alt keys) while clicking on the Edit menu, we'll see something like the menu shown in Figure 7:
Figure 7. The Edit menu of QuickTime Player (Option key down).
Notice that the Paste menu item is now labeled "Add" and the Clear menu item is now labeled "Trim". Similarly, if we hold down just the Shift key while clicking on the Edit menu, we'll see the menu shown in Figure 8:
Figure 8. The Edit menu of QuickTime Player (Shift key down).
Figure 9. The Edit menu of QuickTime Player (Shift and Option keys down).
What's happening here is that QuickTime Player is not using MCGetControllerInfo to do its Edit menu adjusting, at least for the first five menu commands. Instead, it's using the MCSetUpEditMenu function, which is specially designed to change the Edit menu item labels in the ways just described, depending on which keyboard modifier keys the user is holding down. MCSetUpEditMenu is declared essentially like this:
ComponentResult MCSetUpEditMenu (MovieController mc, long modifiers, MenuHandle mh);
MCSetUpEditMenu correctly enables or disables and names the first five commands in the Edit menu specified by the menu handle mh, as long as those items have the standard arrangement (Undo, a separator line, Cut, Copy, Paste, and Clear).
It appears, then, that we can simplify our menu-adjusting code and gain the additional behaviors described above by using MCSetUpEditMenu ourselves. There are just a couple of changes we need to make to support MCSetUpEditMenu. Primarily, we need to add a parameter to our QTFrame_AdjustMenus function, so that we can pass it the current keyboard modifiers. Henceforth, QTFrame_AdjustMenus will be declared like this:
int QTFrame_AdjustMenus (WindowReference theWindow, MenuReference theMenu, long theModifiers);
Getting the appropriate keyboard modifiers in our Macintosh code is easy. Whenever we call QTFrame_AdjustMenus, either we don't care about the modifiers (so we can pass 0L) or we have an event record available (so we can pass (long)theEvent->modifiers).
Getting the Modifier Keys on Windows
When we call QTFrame_AdjustMenus on Windows, however, we need to do some additional work to determine which (if any) modifier keys the user is holding down when clicking on the Edit menu. Remember that we want to pass MCSetUpEditMenu a long integer whose bits indicate which modifier keys are active. The "gotcha" here is that these are supposed to be the modifier keys on a Macintosh keyboard. MCSetUpEditMenu knows nothing about the Alt or Ctrl keys found on Windows keyboards. Rather, it's expecting a 32-bit value in which the up or down state of the relevant modifier keys is encoded using these bits (defined in Events.h):
enum { cmdKey = 1 << cmdKeyBit, // 0x0100 shiftKey = 1 << shiftKeyBit, // 0x0200 alphaLock = 1 << alphaLockBit, // 0x0400 optionKey = 1 << optionKeyBit, // 0x0800 controlKey = 1 << controlKeyBit // 0x1000 };
For example, if only the Option key is down, the modifiers value should be 0x00000800. Similarly, if both the Shift and Control keys are down, the modifiers value should be 0x00001200.
QuickTime maps the Windows modifier keys to the Macintosh modifier keys in this manner:
- The Windows Alt key is mapped to the Macintosh Control key.
- The Windows Ctrl key is mapped to the Macintosh Command key.
- The Windows Shift key is mapped to the Macintosh Shift key.
- The Windows Caps Lock key is mapped to the Macintosh Caps Lock key.
- The combination of the Windows Alt and Ctrl keys is mapped to the Macintosh Option key.
To help us construct a Mac-style modifiers long word, we'll add these constants and compiler macros to the file WinFramework.h:
#define VK_MAC_CONTROLKEY VK_MENU #define VK_MAC_COMMANDKEY VK_CONTROL #define VK_MAC_SHIFTKEY VK_SHIFT #define VK_MAC_CAPSKEY VK_CAPITAL #define QTFrame_IsControlKeyDown(theKeyState) (theKeyState[VK_MAC_CONTROLKEY] & 0x80 ? 1 : 0) #define QTFrame_IsCommandKeyDown(theKeyState) (theKeyState[VK_MAC_COMMANDKEY] & 0x80 ? 1 : 0) #define QTFrame_IsShiftKeyDown(theKeyState) (theKeyState[VK_MAC_SHIFTKEY] & 0x80 ? 1 : 0) #define QTFrame_IsAlphaLockKeyDown(theKeyState) (theKeyState[VK_MAC_CAPSKEY] & 0x80 ? 1 : 0) #define QTFrame_IsOptionKeyDown(theKeyState) (QTFrame_IsControlKeyDown(theKeyState)) && (QTFrame_IsCommandKeyDown(theKeyState))
On Windows, a key state array (represented by the argument theKeyState in these macros) is a 256-byte array that contains information about each of the 256 virtual-key codes. If a key is down, then the high-order bit (0x80) of the corresponding element of this array will be set. For instance, if the Alt key is down, then the high-order bit of the array element whose index is 0x12 will be set. (The virtual-key code for the Alt key is VK_MENU, which is defined as 0x12 in the file Winuser.h.)
We can fill a key state array with the current values by calling the GetKeyboardState function. Then all we need to do is inspect the Windows modifier keys that are of interest to us and construct a Mac-style modifiers value that encodes that information. When we need to call QTFrame_AdjustMenus, we can get the current set of modifier keys by calling QTFrame_GetKeyboardModifiers, defined in Listing 2.
Listing 2: Getting the Windows keyboard modifier keys
QTFrame_GetKeyboardModifiers static long QTFrame_GetKeyboardModifiers (void) { long myModifiers = 0L; BYTE myKeyState[256]; if (GetKeyboardState(&myKeyState[0])) { if (QTFrame_IsOptionKeyDown(myKeyState)) myModifiers |= optionKey; else if (QTFrame_IsCommandKeyDown(myKeyState)) myModifiers |= cmdKey; else if (QTFrame_IsControlKeyDown(myKeyState)) myModifiers |= controlKey; if (QTFrame_IsShiftKeyDown(myKeyState)) myModifiers |= shiftKey; if (QTFrame_IsAlphaLockKeyDown(myKeyState)) myModifiers |= alphaLock; } return(myModifiers); }
So, on Windows, we are now able to pass the correct set of modifier flags to MCSetUpEditMenu. But what do we pass for the third parameter, which on MacOS is a menu handle for the Edit menu? The answer, it turns out, is that we'll pass the value NULL. The reason for this is that on Windows we access our menus using a value of type HMENU, not MenuHandle. This means, however, that on Windows we cannot depend on MCSetUpEditMenu to either highlight or rename the items in the Edit menu. For that, we'll have to write our own code.
Renaming the Edit Menu Items on Windows
At this point, you might be wondering why we're bothering to call MCSetUpEditMenu on Windows, if it isn't going to help us with highlighting or renaming the items in the Edit menu. The answer is that MCSetUpEditMenu does more than simply enable or disable menu items and rename them to match the state of the active modifier keys. MCSetUpEditMenu also sets some flags maintained internally by the movie controller that affect the operation of subsequent editing commands. For instance, when we call MCPaste, it looks at those flags to determine whether it should paste, or replace, or add, or add scaled. In other words, if we don't call MCSetUpEditMenu, all our editing operations will just be the default undo, cut, copy, paste, and clear operations.
On Windows, we still have two tasks left to handle. First, we need to perform our own Edit menu item enabling and disabling. We already have code for this (see Listing 1 again), so we'll just conditionalize that code to be executed under Windows but not under MacOS. Second, we need to find a way to rename the Edit menu items according to the current state of the modifier keys. This task is actually relatively easy, since QuickTime provides the MCGetMenuString function, which we can use to retrieve the label for a particular menu item, given a set of modifier keys. Suppose, for instance, that we execute this line of code (here, myString is of type Str255):
MCGetMenuString(myMC, optionKey, mcMenuPaste, myString);
If MCGetMenuString completes successfully, then myString will hold the string "Add". All we need to do then is insert that string into our Windows Edit menu. The function QTFrame_ConvertMacToWinMenuItemLabel, defined in Listing 3, handles all of this for us.
Listing 3: Renaming a Windows Edit menu item
QTFrame_ConvertMacToWinMenuItemLabel void QTFrame_ConvertMacToWinMenuItemLabel ( MovieController theMC, MenuReference theWinMenu, long theModifiers, UInt16 theMenuItem) { Str255 myString; char *myLabelText = NULL; char *myBeginText = NULL; char *myFinalText = NULL; short myLabelSize = 0; // get the appropriate label for the specified item and keyboard modifiers MCGetMenuString(theMC, theModifiers, MENU_ITEM(theMenuItem), myString); switch (theMenuItem) { case IDM_EDITUNDO: myBeginText = kAmpersandText; myFinalText = kWinUndoAccelerator; break; case IDM_EDITPASTE: myBeginText = ""; myFinalText = kWinPasteAccelerator; break; case IDM_EDITCLEAR: myBeginText = kAmpersandText; myFinalText = kWinClearAccelerator; break; default: // currently, only the Undo, Paste, and Clear items are modified by // MCSetUpEditMenu, so that's all we'll handle here return; } myLabelSize = strlen(myBeginText) + myString[0] + strlen(myFinalText) + 1; myLabelText = malloc(myLabelSize); if (myLabelText == NULL) return; BlockMove(myBeginText, myLabelText, strlen(myBeginText)); BlockMove(&myString[1], myLabelText + strlen(myBeginText), myString[0]); BlockMove(myFinalText, myLabelText + strlen(myBeginText) + myString[0], strlen(myFinalText)); myLabelText[myLabelSize - 1] = '\0'; QTFrame_SetMenuItemLabel(theWinMenu, theMenuItem, myLabelText); free(myLabelText); }
QTFrame_ConvertMacToWinMenuItemLabel also adds the ampersand (&) to the beginning of several of the Edit menu items (so that the first letter is underlined) and the appropriate keyboard accelerator label to the end of all of them.
Putting it All Together
We're finally ready to put all these pieces together. Listing 4 shows our revised version of Listing 1. When no movie controller is available, we disable all the Edit menu items in exactly the same manner we did in our earlier version. And for the "Select All" and "Select None" items, we call MCGetControllerInfo and QTFrame_SetMenuItemState, just like before. But for the five standard Edit menu commands, we now call MCSetUpEditMenu on both Mac and Windows. In addition, on Windows we need to do all menu item enabling and disabling ourselves, and we need to update the menu item labels, using our function QTFrame_ConvertMacToWinMenuItemLabel.
Listing 4: Adjusting the Edit menu (revised version)
QTFrame_AdjustMenus #if TARGET_OS_MAC myMenu = GetMenuHandle(kEditMenuResID); #endif if (myMC == NULL) { // if there is no movie controller, disable all the Edit menu items QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, kDisableMenuItem); } else { MCGetControllerInfo(myMC, &myFlags); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTALL, myFlags & mcInfoEditingEnabled ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, myFlags & mcInfoEditingEnabled ? kEnableMenuItem : kDisableMenuItem); #if TARGET_OS_MAC MCSetUpEditMenu(myMC, theModifiers, myMenu); #endif #if TARGET_OS_WIN32 MCSetUpEditMenu(myMC, theModifiers, NULL); QTFrame_SetMenuItemState(myMenu, IDM_EDITUNDO, myFlags & mcInfoUndoAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCUT, myFlags & mcInfoCutAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, myFlags & mcInfoCopyAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITPASTE, myFlags & mcInfoPasteAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_SetMenuItemState(myMenu, IDM_EDITCLEAR, myFlags & mcInfoClearAvailable ? kEnableMenuItem : kDisableMenuItem); QTFrame_ConvertMacToWinMenuItemLabel (myMC, myMenu, theModifiers, IDM_EDITUNDO); QTFrame_ConvertMacToWinMenuItemLabel (myMC, myMenu, theModifiers, IDM_EDITCUT); QTFrame_ConvertMacToWinMenuItemLabel (myMC, myMenu, theModifiers, IDM_EDITCOPY); QTFrame_ConvertMacToWinMenuItemLabel (myMC, myMenu, theModifiers, IDM_EDITPASTE); QTFrame_ConvertMacToWinMenuItemLabel (myMC, myMenu, theModifiers, IDM_EDITCLEAR); #endif }
There is one final modification that we need to make to our Windows source code. Apparently, on Windows, calling the MCIsPlayerEvent function has the nasty side-effect of clearing the movie controller flags that store the current modifier key settings. So we need to make sure that we do not call MCIsPlayerEvent if we are about to execute an editing command. We can do this by adding the condition (theMessage != WM_COMMAND) in the movie window procedure QTFrame_MovieWndProc. See the version of WinFramework.c included in this month's code for the exact placement of this fix.
Text Importing
Suppose now that we've implemented all the changes described in the previous section. Let's see what all this work has bought us.
Importing Text from the Clipboard
Open a movie with a video track, perhaps even the penguin movie we created in an earlier article. Select part or all of the movie. Then switch to some application that can handle text; in that application, select some text and copy it. Then return to our upgraded application and execute the "Add Scaled" command in the Edit menu (that is, choose Paste while holding down the Shift and Option keys on the Mac, or the Shift and Ctrl and Alt keys on Windows). Voilà — we've just added a text track to our movie, positioned below the video track.
Keep in mind that our upgraded sample applications contain absolutely no special code for handling text media. So how did we manage to create a text track so effortlessly? The answer is that MCPaste looks to see what kind of data it's being asked to insert into the open movie. If it's a segment of a movie, then MCPaste just inserts the data as we'd expect. But if the data isn't movie data, MCPaste looks around for a QuickTime component that can import that kind of data as a movie. In other words, MCPaste goes looking for a suitable movie import component. In this case, it finds the text movie import component (component type MovieImportType and subtype TextMediaType), which inspects the current modifier flags cached by the movie controller and performs the operation corresponding to those flags.
- If none of the relevant modifier flags is set, the text movie importer pastes the text data at the current position in the movie. If a text track already exists in the movie, the pasted text is inserted into that track and inherits all the spatial and visual characteristics of that track. But if no text track exists in the movie, the text movie importer creates a new track that has the same size and position as the current movie box. The pasted text is given a default duration of two seconds.
- If only the Shift modifier flag is set, then MCPaste performs a Replace operation. If the movie has a non-empty selection, the pasted text replaces the current selection; otherwise, if there is no selection, the pasted text replaces the entire movie. In both cases, the duration of the pasted text sample is the default two seconds.
- If only the Option modifier flag is set, then MCPaste performs an Add operation: the text track is positioned below the existing video track, with a height that accommodates the pasted text (this is called adding in parallel). The duration of the pasted text sample is the default two seconds.
- If both the Shift modifier flag and the Option modifier flag are set, then MCPaste performs an Add Scaled operation: a text track is added in parallel for the duration of the current selection. If there is no selection, then the text track is added in parallel for the duration of the entire movie.
On Macintosh operating systems only, holding down the Control key and any other combination of modifier keys while selecting Paste in the Edit menu causes the text movie importer to display the text import settings dialog box, shown in Figure 10. This dialog box allows the user to configure some settings of the pasted text.
Figure 10. The text import settings dialog box Importing Text from a File
The text movie importer can also import text stored in a file, and indeed provides some additional capabilities that are not available when pasting text from the scrap. If we open a text file using any of our sample applications, the text importer creates a movie that has a text sample for every paragraph of text in the file. Each text sample will have the standard default duration of two seconds and will be drawn in the default text font, which is dependent upon the operating system.
Note that, since we're importing a file and not pasting data from the system scrap, our existing sample applications will exhibit this behavior, whether or not we've applied the changes described in the previous section. This is just another case of NewMovieFromFile detecting that the file we've asked it to process is not a QuickTime movie file and then looking around for a suitable movie importer to handle that data. (See "Quick on the Draw" in MacTech, April 2000 for more details on this.)
The text importer recognizes a large number of text descriptors that modify the default characteristics of the imported text. Suppose that we open a text file that contains these lines of text:
{QTtext} {font:Tekton}{plain}{size:18} {textColor: 0, 0, 0}{backColor: 65535, 65535, 0} {justify:center}{timeScale:600}{width:240}{height:40} {timeStamps:absolute}{language:0}{textEncoding:0} {shrinkTextBox: on} [00:00:00.000] {textBox: 10, 0, 30, 240}We forgot to seed! [00:00:01.000] {textBox: 10, 0, 30, 240}D'Oh! [00:00:01.100] {textBox: 10, 20, 30, 240}D'Oh! [00:00:01.200] {textBox: 10, 40, 30, 240}D'Oh! [00:00:01.300] {textBox: 10, 60, 30, 240}D'Oh! [00:00:01.400] {textBox: 10, 80, 30, 240}D'Oh! [00:00:01.500]
The text importer inspects the text descriptors found within the braces and creates the movie whose first frame is shown in Figure 11.
Figure 11. The imported movie.
Unfortunately, we don't have space to investigate text descriptors in more detail here. For complete documentation on using the available text descriptors, see the sources mentioned at the end of this article.
Text Tracks
As we've seen, the text movie importer provides our applications with a good deal of text-handling power at a very small cost. In fact, we didn't have to do anything at all to allow our applications to import text files, and we simply had to upgrade our Edit menu adjusting code to allow them to handle pasted text. But we still need to see how to create text tracks directly, without relying on the text movie importer. After all, we want to be able to work with text data that's not read from a file or from the system scrap.
Adding Text Media Samples
By this point in this series of articles, programmatically adding a track to a movie should be old-hat (since we've done this two or three times so far). We just need to call NewMovieTrack and NewTrackMedia to create a new track and media, call BeginMediaEdits to begin a media-editing session, call AddMediaSample to add samples to the media, call EndMediaEdits to end the media-editing session, and then call InsertMediaIntoTrack to insert the newly-edited media into the track. For any new kind of media that we encounter, we really need to ask only two questions: (1) what is the format of the data in the media samples? And, (2) what is the structure of the sample description that we need to pass to AddMediaSample?
For a text track, the media sample data is just the string of characters in the text itself, preceded by a 16-bit length field that specifies the number of characters in that string. And the appropriate sample description is a text description structure, defined by the TextDescription data type:
struct TextDescription { long descSize; long dataFormat; long resvd1; short resvd2; short dataRefIndex; long displayFlags; long textJustification; RGBColor bgColor; Rect defaultTextBox; ScrpSTElement defaultStyle; char defaultFontName[1]; };
The first five fields, of course, are the first five fields of the generic SampleDescription structure. The remaining fields are specific to text media. The displayFlags field holds a set of flags that indicate how the text is to be displayed. These flags allow us to specify various scrolling options and other positioning options. For the moment, we'll be content to specify the dfClipToTextBox flag, which restricts any updates caused by changes in the text track to the area occupied by the text track. (By all means, however, you should experiment with some of the scrolling options, like dfScrollIn and dfScrollOut.)
The defaultTextBox field specifies the location of the box that encloses the text. The rectangle is interpreted as relative to the upper-left corner of the text track rectangle. The textJustification field contains a value that specifies how the text is to be justified within the text box. The Movie Toolbox recognizes these constants for specifying a text justification (defined in the header file TextEdit.h):
enum { teFlushDefault = 0, teCenter = 1, teFlushRight = -1, teFlushLeft = -2 };
The bgColor field specifies the background color of the text box. The default text color is black. Note that because we call NewHandleClear to allocate a TextDescription structure, the default background color will also be black unless we change the values in the bgColor field. To make the text visible, we'll set the background color to white, like this:
RGBColor myBGColor = {0xffff, 0xffff, 0xffff}; (**mySampleDesc).bgColor = myBGColor;
The last two fields of the TextDescription structure indicate the desired text style and font. We'll ignore these fields here.
Listing 5 shows a segment of the QTText_AddTextTrack function, which we use to add a new text track to a movie. As you can see, it allocates a handle to a text description structure, fills in some of the fields with appropriate values, calls PtrToHand and PtrAndHand to create the text media sample, and then calls AddMediaSample to add the text media sample to the text media.
Listing 5: Adding a text media sample
QTText_AddTextTrack TextDescriptionHandle mySampleDesc = NULL; Handle mySample = NULL; UInt16 myLength; RGBColor myBGColor = {0xffff, 0xffff, 0xffff}; mySampleDesc = (TextDescriptionHandle) NewHandleClear(sizeof(TextDescription)); if (mySampleDesc == NULL) goto bail; (**mySampleDesc).descSize = sizeof(TextDescription); (**mySampleDesc).dataFormat = TextMediaType; (**mySampleDesc).displayFlags = dfClipToTextBox; (**mySampleDesc).textJustification = teCenter; (**mySampleDesc).defaultTextBox = myBounds; (**mySampleDesc).bgColor = myBGColor; myLength = EndianU16_NtoB(mySampleText[0]); // create the text media sample: a 16-bit length word followed by the text myErr = PtrToHand(&myLength, &mySample, sizeof(myLength)); if (myErr == noErr) { myErr = PtrAndHand((Ptr)(&mySampleText[1]), mySample, mySampleText[0]); if (myErr == noErr) AddMediaSample( myMedia, mySample, 0, GetHandleSize(mySample), myTextSampleDuration, (SampleDescriptionHandle)mySampleDesc, 1, 0, NULL); DisposeHandle(mySample); } DisposeHandle((Handle)mySampleDesc);
The Movie Toolbox also provides the TextMediaAddTextSample function, which allows us to simplify this process significantly. Indeed, all of the work done in Listing 5 can be accomplished with this single line of code:
myErr = TextMediaAddTextSample( myHandler, (Ptr)(&mySampleText[1]), mySampleText[0], 0, 0, 0, NULL, NULL, teCenter, &myBounds, dfClipToTextBox, 0, 0, 0, NULL, myTextSampleDuration, NULL);
TextMediaAddTextSample takes seventeen parameters (count 'em!), which is probably some kind of record for a Movie Toolbox function. The payoff for this complexity is that it allows us to dispense with allocating a sample description or a text sample and with worrying about endian issues. Instead, we pass it the text media handler, myHandler (which we can obtain by calling GetMediaHandler on the text media), the text, and a handful of other parameters describing the desired characteristics of the text track.
Positioning a Text Track
When we create a text track, using either AddMediaSample or TextMediaAddTextSample, we need to specify the size and location of the text track. QTText determines the width of the new text track by calling GetTrackDimensions on the first video track in the movie:
GetTrackDimensions(myTypeTrack, &myWidth, &myHeight);
QTText uses the constant kTextTrackHeight (defined as 20 pixels) as the height of the text track.
For below-the-video text, we can specify the position of the text track by setting the track matrix, like this:
GetTrackMatrix(myTextTrack, &myMatrix); TranslateMatrix(&myMatrix, 0, myHeight); SetTrackMatrix(myTextTrack, &myMatrix);
All we've done here is translate the matrix downward by the height of the video track (myHeight). For text that overlays a video track, of course, we'll need to reset the matrix in some other way.
Enabling or Disabling a Text Track
Each track in a QuickTime movie is either enabled or disabled. By default, a newly-created track is enabled, in which case its media data directly contributes to the overall user experience. For example, an enabled video track is visible (unless of course it's completely covered by other enabled tracks), and an enabled audio track is audible. Most other media types, including text media, are visual media types, so once again being enabled means being visible. On the flip side, a disabled track does not usually contribute audible or visible data to the movie. Disabling a track is a quick and easy way to hide or mute it.
We can enable or disable a track by calling the SetTrackEnabled function, passing it a track identifier and a Boolean value that indicates whether to enable (true) or disable (false) the specified track. When we create a text track, we make sure it's visible by enabling it, like this:
SetTrackEnabled(myTextTrack, true);
We can hide the text track by passing false to disable it. Even if a text track is disabled, however, it can still be of use in a movie. For instance, we can search for text in a disabled text track, and the movie controller scans all text tracks, including disabled ones, when looking for chapter tracks. Similarly, the QuickTime Plug-In searches all text tracks, even disabled ones, when looking for an HREF track. Indeed, chapter tracks and HREF tracks are usually disabled.
Creating a Text Track
Listing 6 shows our complete function QTText_AddTextTrack for adding a text track to a movie. The parameter theStrings is an array of C strings; each element of that array is the text for a specific text sample. The parameter theFrames is an array of integers; each element of that array indicates how many video frames a text sample is to span. The sum of all the values in theFrames should equal the total number of frames in the video track. Finally, the isChapterTrack parameter indicates whether the new text track is to be a chapter track; if isChapterTrack is true, then the new text track is attached as a chapter track to the first track whose type is specified by the theType parameter.
Listing 6: Adding a text track
QTText_AddTextTrack Track QTText_AddTextTrack (Movie theMovie, char *theStrings[], short theFrames[], short theNumFrames, OSType theType, Boolean isChapterTrack) { Track myTypeTrack = NULL; Track myTextTrack = NULL; Media myMedia = NULL; MediaHandler myHandler = NULL; TimeScale myTimeScale; MatrixRecord myMatrix; Fixed myWidth; Fixed myHeight; OSErr myErr = noErr; // get the (first) track of the specified type; // this track determines the width of the new text track // and (if isChapterTrack is true) is the target of the new chapter track myTypeTrack = GetMovieIndTrackType(theMovie, 1, theType, movieTrackMediaType); if (myTypeTrack == NULL) goto bail; // get the dimensions of the target track GetTrackDimensions(myTypeTrack, &myWidth, &myHeight); myTimeScale = GetMediaTimeScale (GetTrackMedia(myTypeTrack)); // create the text track and media myTextTrack = NewMovieTrack(theMovie, myWidth, FixRatio(kTextTrackHeight, 1), kNoVolume); if (myTextTrack == NULL) goto bail; myMedia = NewTrackMedia(myTextTrack, TextMediaType, myTimeScale, NULL, 0); if (myMedia == NULL) goto bail; myHandler = GetMediaHandler(myMedia); if (myHandler == NULL) goto bail; // figure out the text track geometry GetTrackMatrix(myTextTrack, &myMatrix); TranslateMatrix(&myMatrix, 0, myHeight); SetTrackMatrix(myTextTrack, &myMatrix); SetTrackEnabled(myTextTrack, true); // edit the track media myErr = BeginMediaEdits(myMedia); if (myErr == noErr) { Rect myBounds; short myIndex; TimeValue myTypeSampleDuration; TimeRecord myTimeRec; myBounds.top = 0; myBounds.left = 0; myBounds.right = Fix2Long(myWidth); myBounds.bottom = Fix2Long(myHeight); // determine the duration of a sample in the track of the specified type myTypeSampleDuration = QTUtils_GetFrameDuration(myTypeTrack); for (myIndex = 0; myIndex < theNumFrames; myIndex++) { TimeValue myTextSampleDuration; Str255 mySampleText; myTextSampleDuration = myTypeSampleDuration * theFrames[myIndex]; // set the time scale of the media to that of the movie myTimeRec.value.lo = myTextSampleDuration; myTimeRec.value.hi = 0; myTimeRec.scale = GetMovieTimeScale(theMovie); ConvertTimeScale(&myTimeRec, GetMediaTimeScale(myMedia)); myTextSampleDuration = myTimeRec.value.lo; QTText_CopyCStringToPascal(theStrings[myIndex], mySampleText); // Listing 5 omitted at this point, for space reasons // write out the new data to the media myErr = TextMediaAddTextSample( myHandler, (Ptr)(&mySampleText[1]), mySampleText[0], 0, 0, 0, NULL, NULL, teCenter, &myBounds, dfClipToTextBox, 0, 0, 0, NULL, myTextSampleDuration, NULL); } } myErr = EndMediaEdits(myMedia); if (myErr != noErr) goto bail; // insert the text media into the text track myErr = InsertMediaIntoTrack(myTextTrack, 0, 0, GetMediaDuration(myMedia), fixed1); if (myErr != noErr) goto bail; // set the text handling procedure TextMediaSetTextProc(myHandler, gTextProcUPP, (long)QTFrame_GetWindowObjectFromFrontWindow()); // if desired, set the new text track as a chapter track for the track of the specified type if (isChapterTrack) AddTrackReference(myTypeTrack, myTextTrack, kTrackReferenceChapterList, NULL); bail: return(myTextTrack); }
For the moment, you can ignore the calls to TextMediaSetTextProc and AddTrackReference. We'll explain them a little later.
Text Searching
The Movie Toolbox provides several functions that we can use to search for a specific word or series of words in a text track. If we are interested in simply finding out where in a text track the next occurrence of a string is located, we can use the TextMediaFindNextText function, like this:
myTimeValue = GetMovieTime(myMovie, NULL); myErr = TextMediaFindNextText( myHandler, (Ptr)(&theText[1]), theText[0], myFlags, myTimeValue, &myFoundTime, &myFoundDuration, &gOffset);
The first parameter, myHandler, is the text media handler associated with the text track. The second and third parameters specify the text to be searched for and the length of that text; here we're supposing that the text is contained in the variable theText, which is a Pascal string. The fourth parameter is a set of search flags, which indicate how TextMediaFindNextText is to search for the specified text. These flags are defined:
enum { findTextEdgeOK = 1 << 0, findTextCaseSensitive = 1 << 1, findTextReverseSearch = 1 << 2, findTextWrapAround = 1 << 3, findTextUseOffset = 1 << 4 };
These constants are pretty much self-explanatory, except for the first and the last. If findTextEdgeOK is set in the search flags, then TextMediaFindNextText will match text beginning at the movie time specified by the fifth parameter; otherwise, the text must occur in some later (or earlier, if findTextReverseSearch is set) sample. If findTextUseOffset is set, then TextMediaFindNextText will search beginning at the offset specified by the last parameter. This allows us to find separate occurrences of the search text in a single text sample.
Our QTText sample application maintains a couple of global variables that keep track of the kind of search the user wants to perform. We'll use those variables to set our search flags like this:
myFlags = findTextUseOffset; if (!gSearchForward) myFlags |= findTextReverseSearch; if (gSearchWrap) myFlags |= findTextWrapAround; if (gSearchWithCase) myFlags |= findTextCaseSensitive;
If TextMediaFindNextText finds the text specified by the second and third parameters in some text sample, it returns the movie time of the beginning of that sample in the sixth parameter (here, &myFoundTime). It also returns the duration of that sample in the seventh parameter and, in the last parameter, the byte offset (from the beginning of the text portion of that sample) of the first character of that text.
Typically, we don't just want to find out where some text begins; we also want to advance the movie to that point and highlight the found text. We can use the MCDoAction function with the mcActionGoToTime action to set the current movie time to the time returned to us by TextMediaFindNextText, like so:
myNewTime.value.hi = 0; myNewTime.value.lo = myFoundTime; myNewTime.scale = GetMovieTimeScale(myMovie); myNewTime.base = NULL; // go to the found text MCDoAction(myMC, mcActionGoToTime, &myNewTime);
And we can use the TextMediaHiliteTextSample function to highlight the selected text:
myColor.red = myColor.green = myColor.blue = 0x8000; // grey TextMediaHiliteTextSample(myHandler, myFoundTime, gOffset, gOffset + theText[0], &myColor);
Once again, however, the Movie Toolbox provides a function that greatly simplifies our work here. The MovieSearchText function, introduced in QuickTime version 2.0, finds the text, sets the movie time to the beginning of the text sample containing that text, and highlights the found text in that sample. So we can replace all the code we've encountered so far in this section with this single line of code:
myErr = MovieSearchText( myMovie, (Ptr)(&theText[1]), theText[0], myFlags, NULL, &myTimeValue, &gOffset);
When we call MovieSearchText, we pass in the movie to search, the search text and search text length, a set of flags, the first text track to search, and the movie time at which to start the search. The set of flags can include any of the search flags listed above, as well as any of these additional flags that are specific to the MovieSearchText function:
enum { searchTextDontGoToFoundTime = 1L << 16, searchTextDontHiliteFoundText = 1L << 17, searchTextOneTrackOnly = 1L << 18, searchTextEnabledTracksOnly = 1L << 19 };
Including either of the first two flags, searchTextDontGoToFoundTime and searchTextDontHiliteFoundText, allows us to override the default "go-to-and-highlight" behavior of MovieSearchText. The next two flags modify the track-searching behavior. If we pass a track identifier in the fifth parameter, then MovieSearchText will search only that track if the searchTextOneTrackOnly flag is set; otherwise, it will search all text tracks in the specified movie, starting with that track. We can restrict the search to all enabled text tracks by setting the searchTextEnabledTracksOnly flag.
If MovieSearchText finds the specified text, it returns the movie time of the text sample in which the text was found and the byte offset within that sample of the found text. It also returns the track identifier of the track containing the text sample, unless the track parameter was set to NULL on input.
Listing 7 contains the definition of our function QTText_FindText, which we use to search for text. As you can see, it uses either the TextMediaFindNextText function or the MovieSearchText function, depending on the value of the compiler flag USE_MOVIESEARCHTEXT.
Listing 7: Finding some text
QTText_FindText void QTText_FindText (WindowObject theWindowObject, Str255 theText) { ApplicationDataHdl myAppData = NULL; Movie myMovie = NULL; MediaHandler myHandler = NULL; MovieController myMC = NULL; long myFlags = 0L; TimeValue myTimeValue; OSErr myErr = noErr; myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); if (myAppData == NULL) return; myMC = (**theWindowObject).fController; myMovie = (**theWindowObject).fMovie; myHandler = (**myAppData).fTextHandler; // set the search features myFlags = findTextUseOffset; if (!gSearchForward) myFlags |= findTextReverseSearch; if (gSearchWrap) myFlags |= findTextWrapAround; if (gSearchWithCase) myFlags |= findTextCaseSensitive; myTimeValue = GetMovieTime(myMovie, NULL); #if USE_MOVIESEARCHTEXT myFlags |= searchTextEnabledTracksOnly; myErr = MovieSearchText(myMovie, (Ptr)(&theText[1]), theText[0], myFlags, NULL, &myTimeValue, &gOffset); if (myErr != noErr) QTFrame_Beep(); // if the desired string wasn't found, beep #else if (myHandler != NULL) { TimeValue myFoundTime, myFoundDuration; TimeRecord myNewTime; RGBColor myColor; myColor.red = myColor.green = myColor.blue = 0x8000;// grey // search for the specified text myErr = TextMediaFindNextText(myHandler, (Ptr)(&theText[1]), theText[0], myFlags, myTimeValue, &myFoundTime, &myFoundDuration, &gOffset); if (myFoundTime != -1) { // convert the TimeValue to a TimeRecord myNewTime.value.hi = 0; myNewTime.value.lo = myFoundTime; myNewTime.scale = GetMovieTimeScale(myMovie); myNewTime.base = NULL; // go to the found text MCDoAction(myMC, mcActionGoToTime, &myNewTime); // highlight the text TextMediaHiliteTextSample(myHandler, myFoundTime, gOffset, gOffset + theText[0], &myColor); } else { QTFrame_Beep(); // if the desired string wasn't found, beep } } #endif // update the current offset, if we're searching forward if (gSearchForward && (myErr == noErr)) gOffset += theText[0]; }
Of course, your code won't need to use this compiler flag; you'll call just MovieSearchText or TextMediaFindNextText for your text searching. Here we simply want to illustrate how to call both of these functions.
Text Editing
Let's consider now how to edit the data in a text track. Conceptually, this is a fairly simple operation. We can just call DeleteTrackSegment to delete one or more existing text samples from a track; then we can call TextMediaAddTextSample to add a new text sample to the text media and then InsertMediaIntoTrack to place that text sample at the desired location in the track. For the moment, we'll limit ourselves to replacing a single existing text sample by another sample that occupies the same location in the track (that is, that has the same starting point and duration as the original sample).
When the user selects the "Edit Current Text..." menu item, we'll display the dialog box shown in Figure 12. If the user clicks the OK button, we'll retrieve the text from the edit text control in that dialog box and use that text as the replacement text data. There are only two things we still need to figure out: (1) how can we get the text of the current text sample (to put into the dialog box when it's first displayed)? And, (2) how can we determine the starting time and duration of the current text sample? Let's take these tasks in order.
Figure 12. The Edit Text dialog box Getting the Current Text
The first task is the easier of the two, mainly because whenever the text media handler is about to display a new text sample, it calls an application-defined text callback procedure that we've previously installed by calling TextMediaSetTextProc (in Listing 6). The text callback procedure is passed several parameters, one of which is a handle to the sample data of the current media sample. So all we need to do is make a copy of the sample text in a place we can find it when we are about to display the dialog box shown in Figure 12. Listing 8 shows our application's text callback procedure.
Listing 8: Getting the current text
QTText_TextProc PASCAL_RTN OSErr QTText_TextProc (Handle theText, Movie theMovie, short *theDisplayFlag, long theRefCon) { #pragma unused(theMovie, theRefCon) char *myTextPtr = NULL; short myTextSize; short myIndex; // on entry to this function, theText is a handle to the text sample data, // which is a big-endian 16-bit length word followed by the text itself myTextSize = EndianU16_BtoN(*(short *)(*theText)); myTextPtr = (char *)(*theText + sizeof(short)); // copy the text into our global variable for (myIndex = 1; myIndex <= myTextSize; myIndex++, myTextPtr++) gSampleText[myIndex] = *myTextPtr; gSampleText[0] = myTextSize; // ask for the default text display *theDisplayFlag = txtProcDefaultDisplay; return(noErr); }
As you can see, we first parse the sample data to get the 16-bit length field and the location of the first character in the text string. Then we copy the characters into the global variable gSampleText, which is of type Str255. Finally, we return the value txtProcDefaultDisplay in the parameter theDisplayFlag; this instructs the text media handler to use the display flags contained in the displayFlags field of the text description structure for that text sample. (There are also constants to force the sample to be shown or not shown, regardless of the media's default display flags.)
Finding Sample Boundaries
Now we need to figure out how to find the starting time and duration of the current text media sample (so we know what segment of the text track to replace). An easy way to get the starting time would be to call GetMovieTime in our text callback procedure and then assign the returned value to a global variable. A better way — because it can be used with media types other than text — is to call the GetTrackNextInterestingTime function inside the QTText_EditText function. GetTrackNextInterestingTime allows us to search for specific times in a track, given a set of search criteria. The search criteria are specified by these flags:
enum { nextTimeMediaSample = 1 << 0, nextTimeMediaEdit = 1 << 1, nextTimeTrackEdit = 1 << 2, nextTimeSyncSample = 1 << 3, nextTimeStep = 1 << 4, nextTimeEdgeOK = 1 << 14, nextTimeIgnoreActiveSegment = 1 << 15 };
For present purposes, we'll use the two flags nextTimeMediaSample and nextTimeEdgeOK, which tell GetTrackNextInterestingTime to search in the next sample in the track's media but to consider samples that begin or end at the search starting time.
We'll begin by getting the current movie time (which might not be the beginning of the current text sample), like this:
myMovieTime = GetMovieTime(myMovie, NULL);
Then we want to search backward to find the beginning of the current media sample:
GetTrackNextInterestingTime( myTrack, nextTimeEdgeOK | nextTimeMediaSample, myMovieTime, -fixed1, &myInterestingTime, NULL);
The third parameter specifies the starting time for the search and the fourth parameter indicates the direction of the search; because the value here is negative, the search goes backwards from the current movie time. Once GetTrackNextInterestingTime finds the beginning of the current media sample, it returns that time in the location pointed to by the fifth parameter. We've set the sixth parameter to NULL because we don't need the duration from the current time to the found interesting time to be returned to us.
So we've found the beginning of the current text sample. We can find the duration of that sample by calling GetTrackNextInterestingTime once more, this time searching forward from the beginning of the sample, like this:
myMovieTime = myInterestingTime; GetTrackNextInterestingTime( myTrack, nextTimeEdgeOK | nextTimeMediaSample, myMovieTime, fixed1, NULL, &myDuration);
In this case, we want only the duration of the sample returned to us, so we pass NULL in the fifth parameter and &myDuration in the sixth.
Keep in mind that the time values that GetTrackNextInterestingTime returns to us are in the movie time scale. This is useful, since the parameters to DeleteTrackSegment must also be in the movie time scale. So we can now call DeleteTrackSegment to remove the current text sample from the track:
myErr = DeleteTrackSegment(myTrack, myInterestingTime, myDuration);
All that remains is to add a new text sample in place of the one we just removed. For this, we can call TextMediaAddTextSample as we did earlier. There is only one complication here: the duration we pass to TextMediaAddTextSample must be expressed in the media time scale, not the movie time scale. But the Movie Toolbox conveniently provides the MediaTimeToSampleNum function that we can use to get the start time and duration of the current media sample in the media time scale, like this:
myMovieTime = GetMovieTime(myMovie, NULL); myMediaCurrentTime = TrackTimeToMediaTime (myMovieTime, myTrack); MediaTimeToSampleNum( myMedia, myMediaCurrentTime, &myMediaSampleIndex, &myMediaSampleStartTime, &myMediaSampleDuration);
So, we've got all the information we need to call TextMediaAddTextSample and InsertMediaIntoTrack and thereby complete the text sample editing operation. For the complete definition of QTText_EditText, see the file QTText.c.
Chapter Tracks
We learned earlier that a chapter track is just a text track that has been associated in a particular way with some other track. Let's call this other track the target track. We create the association between a text track and the target track by creating a track reference from that target to the text track. In general, a track reference is simply a way for one track to establish a relationship with some other track. The type of the track reference indicates the nature of that relationship. The Movie Toolbox currently provides three constants for track reference types:
enum { kTrackReferenceChapterList = FOUR_CHAR_CODE('chap'), kTrackReferenceTimeCode = FOUR_CHAR_CODE('tmcd'), kTrackReferenceModifier = FOUR_CHAR_CODE('ssrc') };
A track reference of type kTrackReferenceChapterList is used to create a chapter track. A track reference of type kTrackReferenceTimeCode is used to create a timecode track, in which timecode values are associated with the samples of the target track. (We'll consider timecode tracks in more detail in the next QuickTime Toolkit article.) A track reference of type kTrackReferenceModifier is used to create a modifier track; modifier tracks are useful when you want one track to modify the appearance or behavior of a target track. For instance, a tween track is a kind of modifier track that can be used to change, say, the volume of a sound track as the movie progresses. We'll encounter modifier tracks in several upcoming articles, to change the current image of a sprite track and to apply a special effect to a video track. Other parts of QuickTime define additional types of track references. For example, the file QuickTimeVRFormat.h defines several types of track references that are used in building QuickTime VR movie files.
It's actually a rather trivial operation to create a chapter track once we have a text track at hand. If myTypeTrack is a track identifier for a target track (in the present case, a video track), then we can create a chapter track reference to our text track like this:
AddTrackReference(myTypeTrack, myTextTrack, kTrackReferenceChapterList, NULL);
The last parameter is a pointer to a long word in which AddTrackReference will return the index assigned to the new track reference; we don't need this information, so we set that parameter to NULL.
All the chapter titles must be contained in a single text track; we specify the starting time for chapters when we add the text to the text track by calling TextMediaAddTextSample. Note that we need to create the chapter association only between the text track and one other target track, not between the text track and all other tracks in the movie. The target track must be enabled, but typically the chapter track is not enabled (unless we want the text track to be visible).
It's also very easy to remove a track reference and hence to change a chapter track back into a non-chapter text track. Again, if myTypeTrack is the target track, then we can disassociate it from the text track by calling DeleteTrackReference, like this:
DeleteTrackReference(myTypeTrack, kTrackReferenceChapterList, 1);
Here the last parameter is the index of the track reference of the specified type that we want to remove. Our code attaches at most one chapter track to a target, so we can safely set that index to 1.
Listing 9 shows our complete function for turning a text track into a chapter track or turning a chapter track back into a text track. The Boolean parameter isChapterTrack determines whether the first text track in the movie becomes a chapter track or is demoted from that lofty rank.
Listing 9: Setting and unsetting chapter tracks
QTText_SetTextTrackAsChapterTrack OSErr QTText_SetTextTrackAsChapterTrack (WindowObject theWindowObject, OSType theType, Boolean isChapterTrack) { ApplicationDataHdl myAppData = NULL; Movie myMovie = NULL; MovieController myMC = NULL; Track myTypeTrack = NULL; Track myTextTrack = NULL; OSErr myErr = paramErr; // get the movie, controller, and related stuff myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject(theWindowObject); if (myAppData == NULL) return(myErr); myMovie = (**theWindowObject).fMovie; myMC = (**theWindowObject).fController; myTextTrack = (**myAppData).fTextTrack; if ((myMovie != NULL) && (myMC != NULL)) { myTypeTrack = GetMovieIndTrackType(myMovie, 1, theType, movieTrackMediaType | movieTrackEnabledOnly); if ((myTypeTrack != NULL) && (myTextTrack != NULL)) { // add or delete a track reference, as determined by the desired final state if (isChapterTrack) myErr = AddTrackReference(myTypeTrack, myTextTrack, kTrackReferenceChapterList, NULL); else myErr = DeleteTrackReference(myTypeTrack, kTrackReferenceChapterList, 1); // tell the movie controller we've changed aspects of the movie MCMovieChanged(myMC, myMovie); // stamp the movie as dirty (**theWindowObject).fIsDirty = true; } } return(myErr); }
Note that after we call AddTrackReference or DeleteTrackReference, we need to call MCMovieChanged to inform the movie controller that we've changed the associated movie. This prompts the movie controller to redraw the movie controller bar to show or hide the chapter pop-up menu.
The file QTText.c contains a number of other chapter track utilities. Listing 10 defines the one we use for determining whether a track is a chapter track; we call this function when we need to determine whether to place a check mark next to the "Chapter Track" menu item.
Listing 10: Determining whether a track is a chapter track
QTText_IsChapterTrack Boolean QTText_IsChapterTrack (Track theTrack) { Movie myMovie = NULL; Track myTrack = NULL; long myTrackCount = 0L; long myTrRefCount = 0L; long myTrackIndex; long myTrRefIndex; myMovie = GetTrackMovie(theTrack); if (myMovie == NULL) return(false); myTrackCount = GetMovieTrackCount(myMovie); for (myTrackIndex = 1; myTrackIndex <= myTrackCount; myTrackIndex++) { myTrack = GetMovieIndTrack(myMovie, myTrackIndex); if ((myTrack != NULL) && (myTrack != theTrack)) { // iterate thru all track references of type kTrackReferenceChapterList myTrRefCount = GetTrackReferenceCount(myTrack, kTrackReferenceChapterList); for (myTrRefIndex = 1; myTrRefIndex <= myTrRefCount; myTrRefIndex++) { Track myRefTrack = NULL; myRefTrack = GetTrackReference(myTrack, kTrackReferenceChapterList, myTrRefIndex); if (myRefTrack == theTrack) return(true); } } } return(false); }
Hypertext Reference Tracks
A hypertext reference track, or HREF track, is a text track in which some or all of the samples contain hypertext links, in the form of URLs. (Actually, there's no requirement that any of the samples in an HREF track contain a hypertext link, but then of course it's not very useful.) These URLs can be any kind of URL supported by QuickTime, including HTTP, HTTPS, FTP, file, RTSP, and JavaScript URLs. Indeed, if the QuickTime Plug-In finds a URL it doesn't recognize, it passes it to the web browser for processing. So, really, the sky's the limit in terms of the kind of URLs we can put in an HREF track.
From a programming perspective, creating an HREF track is even easier than creating a chapter track. All we need to do is set the name of a text track to "HREFTrack". The plug-in interprets the first text track in a movie having that name as the active HREF track. Listing 11 defines the function QTText_SetTextTrackAsHREFTrack that we can use to set and unset a text track as an HREF track.
Listing 11: Setting and unsetting HREF tracks
QTText_SetTextTrackAsHREFTrack OSErr QTText_SetTextTrackAsHREFTrack (Track theTrack, Boolean isHREFTrack) { OSErr myErr = noErr; myErr = QTUtils_SetTrackName(theTrack, isHREFTrack ? kHREFTrackName : kNonHREFTrackName); return(myErr); }
A track's name is stored as part of the track's user data, so QTUtils_SetTrackName (defined in QTUtilities.c) calls SetUserDataItem to set the name. In QTText_SetTextTrackAsHREFTrack, we use these constants for the track names:
#define kHREFTrackName "HREFTrack" #define kNonHREFTrackName "Text Track"
Ideally, each track should have a unique name (though this is not required). So instead of hard-coding the name for the non-HREF track, we can generate a track name dynamically, looking at the names that are already assigned to tracks in the movie. QTUtilities.c defines a function, QTUtils_MakeTrackNameByType, that we can call to accomplish this. Reworking QTText_SetTextTrackAsHREFTrack to use QTUtils_MakeTrackNameByType is left as an exercise for the reader.
Occasionally it's useful to know whether a specified text track is an HREF track. (For instance, QTText needs to know this to decide whether to put a check mark beside the "HREF Track" menu item.) The function QTText_IsHREFTrack, defined in Listing 12, returns a Boolean value that indicates whether a given text track is an HREF track.
Listing 12: Determining whether a track is an HREF track
QTText_IsHREFTrack Boolean QTText_IsHREFTrack (Track theTrack) { Boolean isHREFTrack = false; char *myTrackName = NULL; myTrackName = QTUtils_GetTrackName(theTrack); if (myTrackName != NULL) isHREFTrack = (strcmp(myTrackName, kHREFTrackName) == 0); free(myTrackName); return(isHREFTrack); }
Some Loose Ends
Let's finish up by taking care of a few loose ends in our basic application framework that become apparent when we start working with text tracks. As you know, when we paste some data into a movie, the current movie time is set to the time immediately following the pasted data. (This is the standard behavior with any kind of pasting.) If pasting causes the movie box to expand, it might happen that the expanded portion of the movie box in the current frame contains areas that should be erased but which are not erased by the movie controller. For example, if we've added some text in parallel, we might see something like Figure 13. Here, the video media handler has redrawn the video portion of the movie box but the text media handler, thinking (correctly) that there's no text for the current movie time, has left the text portion of the movie box untouched. As a result, the image of the movie controller bar, which used to occupy the space now occupied by the text track, is not erased. This is not good, but it's easy enough to fix.
Figure 13. A movie window after adding in parallel.
When some part of the movie box needs to be redrawn, an update event is generated for that portion of the movie box. When we pass that event to MCIsPlayerEvent, the movie controller redraws the appropriate portion of the movie and clears that area from the update region of the window. The problem, as we've just seen, is that the movie controller doesn't think that the bottom portion needs to be redrawn and hence doesn't redraw it. We can solve this problem by erasing that portion of the window ourselves. Listing 13 shows our updated version of QTApp_Draw.
Listing 13: Redrawing a movie window
QTApp_Draw void QTApp_Draw (WindowReference theWindow) { GrafPtr mySavedPort = NULL; GrafPtr myWindowPort = NULL; WindowPtr myWindow = NULL; Rect myRect; GetPort(&mySavedPort); myWindowPort = QTFrame_GetPortFromWindowReference(theWindow); myWindow = QTFrame_GetWindowFromWindowReference(theWindow); if (myWindowPort == NULL) return; MacSetPort(myWindowPort); #if TARGET_API_MAC_CARBON GetPortBounds(myWindowPort, &myRect); #else myRect = myWindowPort->portRect; #endif BeginUpdate(myWindow); if (QTFrame_IsDocWindow(theWindow)) EraseRect(&myRect); // ***insert application-specific drawing here*** EndUpdate(myWindow); MacSetPort(mySavedPort); }
As you can see, we call EraseRect on the entire window rectangle. Keep in mind, however, that BeginUpdate limits the redrawn portion to the intersection of the visible region of the window and the current update region. Since MCIsPlayerEvent will already have removed the active movie region from the update region, our call to EraseRect just redraws the visible portion of the update region that wasn't redrawn by the movie controller.
The last thing we need to do is make sure that the entire movie box is included in the update region when we call MCIsPlayerEvent. We can accomplish this by adding a few lines to our QTFrame_HandleEditMenuItem function. Essentially, we need to make sure to invalidate the entire movie box whenever the size of the movie box might have changed. Listing 14 shows the lines we'll add to the end of QTFrame_HandleEditMenuItem.
Listing 14: Invalidating a movie window
QTFrame_HandleEditMenuItem // if the size of the movie might have changed, invalidate the entire movie box if ((theMenuItem == IDM_EDITUNDO) || (theMenuItem == IDM_EDITCUT) || (theMenuItem == IDM_EDITPASTE) || (theMenuItem == IDM_EDITCLEAR)) { Rect myRect; #if TARGET_OS_WIN32 RECT myWinRect; #endif MCGetControllerBoundsRect(myMC, &myRect); #if TARGET_OS_MAC InvalWindowRect(QTFrame_GetWindowFromWindowReference (theWindow), &myRect); #endif #if TARGET_OS_WIN32 QTFrame_ConvertMacToWinRect(&myRect, &myWinRect); InvalidateRect(theWindow, &myWinRect, false); #endif }
With these changes made, we should see no glitches like those in Figure 13.
Conclusion
We've covered a fair amount of ground in this article. We've seen how to create text tracks, chapter tracks, and HREF tracks; we've also learned how to search a text track and edit the data in a text track. Along the way, we've seen how to upgrade our Edit menu item adjusting and our movie window redrawing, so that even our applications that are not directly concerned with text can import text from files or from the system scrap.
We've also learned a more general lesson: QuickTime often provides more than one way to accomplish some particular task. We've seen, for instance, that we can call either AddMediaSample or TextMediaAddTextSample to add media samples to a text track. And, we can call either TextMediaFindNextText or MovieSearchText to search for text within a text track. Which of these functions we use in any particular case is a matter of taste, no doubt, but also a matter of simplicity and code size. TextMediaAddTextSample and MovieSearchText hold the clear advantage when we consider the amount of source code we need to write and the kinds of details (like endian issues) that we need to attend to. In the future, we'll generally opt for the simpler, cleaner way of solving our programming tasks (and leave the dinosaur bones for the archeologists).
Acknowledgements and References
Thanks are due once again to Brian Friedkin, for his ever-helpful guidance on Windows-related issues. Some of the code in QTText is based on an earlier sample code package by Nick Thompson; you can find an explanation of that code in his article in develop, Issue 20 (archived at http://www.mactech.com/articles/develop/issue_20/20quicktime.html).
You can find a thorough explanation of text descriptors at http://www.apple.com/quicktime/authoring/textdescriptors.html; an even more readable account is found in Steve Gulie's indispensable book QuickTime for the Web (available at http://www.devdepot.com/ and at all good bookstores).
Tim Monroe works in the QuickTime Engineering group at Apple. You can contact him at monroe@apple.com.
![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/all/themes/custom_front/img/search_text.gif)
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine