QuickTime Toolkit
Volume One: Basic Movie Playback and Media Types Apple
AMSTERDAM * BOSTON 9HEIDELBERG ~ L O N D O N N E W Y O R K ~ O X F O R D * PARIS * SAN D I E G O SAN F R A N C I S C O * S I N G A P O R E ~ S Y D N E Y * T O K Y O
ELSEVIER
M o r g a n K a u f m a n n Publishers is an i m p r i n t o f Elsevier
MORGAN
KAUFMANN
PUBLISHERS
QuickTime Developer Series Apple's QuickTime is a way to deliver multimedia--video, sound, styled text, MIDI, Flash, virtual reality, 3D models, sprites, and more--wrapped in a package that will play on Windows or Macintosh computers, on CD-ROM, over the Internet, in a browser window, a PDF document, a PowerPoint presentation, a Word document, or all by itself. The Q u i c k T i m e D e v e l o p e r Series, developed in close cooperation with Apple, is devoted to exploring all of the capabilities of this powerful industry standard. Books in the series are written by experienced developers, including engineers from within the development team at Apple. All of the books feature a practical, hands-on approach and are prepared according to the latest developments in QuickTime technology. QuickTime Toolkit, Volume One: Basic Movie Playback and Media Types Apple Q uickTime Toolkit, Volume 7h,o: Advanced Movie Playback and Media ~pes Apple Interactive QuickTime: Authoring Wired Media Matthew Peterson QuickTime for the Web: For Windows and Macintosh, Third Edition Apple
I know a lot about O uickTime, but [the author] seems to know more: little nuggets of information that can be very valuable in the future--things you didn't even know you didn't know. I wish I had this book when I was just starting out working with Quick Time. It would have saved me a lot of time trying to figure things out on my own. --Steve Israelson VP Technology, TotallyHip Inc. Ever found yourself floundering in API documentation, samples, and developer lists when it comes to implementing one or other of the many wonderful QuickTime features? Well flounder no longer/The ever-practical QuickTime Developer Series comes to the rescue with QuickTime Toolkit: everything you need to know to get a host of Quick Time features actually working for you, presented in its customary lucid and down-to-earth style. Unquestionably the first port of call for anyone contemplating Quick Time development at the A l l level on OS X, Windows, or both, and--with chapters covering such topics as Data Handlers, Carbon Events, cross-platform issues, and error handling--an invaluable resource even for seasoned Quick Time developers. --John Cromie Skylark Associates Ltd. I just got [this] book in the mail and spent a few hours reading it . . . . Finally, good documentation on some of the Quick Time functions that have been so mysterious/ I'm going to look at that sprite override code for making panning videos . . . . [The author] has been and remains a great source of information on Quick Time. He has a uniquely proactive creative~logical approach demonstrating the great possibilities in a great technology. --Bill Meikle programmer, vrhotwires.com
Senior Editor Publishing Services Manager Project Editor Project Management Editorial Coordinator Cover Design Cover Image~Photo Series Text Design Composition Illustration Copyeditor Proofreader Indexer Interior Printer Cover Printer
Tim Cox Andr6 Cuello Anne B. McGee Elisabeth Belier Rick Camp Laurie Anderson 9 Digital Vision/Getty Images Rebecca Evans Nancy Logan Dartmouth Publishing, Inc. Yonie Overton Jennifer McClain Jay Kreider The Maple-Vail Book Manufacturing Group Phoenix Color Corporation
9 2004 by Apple Computer, Inc. All rights reserved. Apple, the Apple logo, and QuickTime are trademarks of Apple Computer, Inc., registered in the United States and other countries, used by Morgan Kaufmann under license. The QuickTime logo is a trademark of Apple Computer, Inc., used by Morgan Kaufmann under license. Designations used by companies to distinguish their products are often claimed as trademarks or registered trademarks. In all instances in which Morgan Kaufmann Publishers is aware of a claim, the product names appear in initial capital or all capital letters. Readers, however, should contact the appropriate companies for more complete information regarding trademarks and registration. Morgan Kaufmann Publishers is an imprint of Elsevier. 500 Sansome Street, Suite 400, San Francisco, CA 94111 No part of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means--electronic, mechanical, photocopying, scanning, or otherwise--without prior written permission of the publisher. Permissions may be sought directly from Elsevier's Science & Technology Rights Department in Oxford, UK: phone: {+ 44} 1865 843830, fax: {+ 44} 1865 853333, e-mail:
[email protected]. You may also complete your request on-line via the Elsevier homepage {http://elsevier.com} by selecting Customer Support and then Obtaining Permissions.
Library of Congress Cataloguing-in-Publication Application submitted. ISBN: 0-12-088401-1 For information on all Morgan Kaufmann publications, visit our website at www.mkp.com. Printed in the United States of America 04 05 06 07 08
5 4 3 2 1
This book is printed on acid-free paper.
Contents Preface Development Platforms How to Read This Book
XV
xvi xvii
xviii
Acknowledgements Chapter I
It All Starts Today Introduction 1 Movie Controllers 1 The Application Framework 4 Handling Movie Windows 6 Handling Menus 11 QuickTime Support 13 Opening a Movie File 13 Attaching a Movie Controller 14 Handling Events 17 Editing a Movie 18 Conclusion 20
Chapter 2
21
Control Introduction 21 Managing the Controller Bar 22 Showing and Hiding the Controller Bar 22 Using Badges 24 Attaching and Detaching the Controller Bar 25 Managing Controller Bar Buttons 28 Managing the Standard Controller Buttons 28 Managing the QuickTime VR Controller Buttons Using the Controller Bar Custom Button 32 Selecting an Entire Movie 36 Movie User Data 38 Specifying the Controller Type 39 Manipulating a Movie's Looping State 43 Getting a Movie's Stored Window Position 46
29
Contents
v
Opening URLs 48 Conclusion 50 Chapter 3
51
Out of Control
Introduction 51 Getting Started 55 Setting Up to Use QuickTime 55 Using Application-Specific Data 56 Updating the Application Framework 59 Preparing Movies for Playback 62 Prerolling Movies 62 Preprerolling Movies 63 Playing Movies 65 Editing Movies 67 Looping Movies 71 Playing Picture-in-Picture Movies 76 Setting the Picture-in-Picture Movie Geometry 76 Setting the Main Movie's Display Clipping Region 77 Playing the Picture-in-Picture Movie 79 Moving the Picture-in-Picture Movie 81 Conclusion 83 Chapter 4
Chapter 5
vi
Contents
The Image Introduction 85 Importing Images 87 Expanding the Application Framework 91 Transforming Images 95 Working with Image Matrices 96 Flipping Images 98 Rotating Images 100 Scaling Images 102 Working with Multi-image Files 105 Exporting Images 106 Exporting Images Using the Export Image Dialog Box Exporting Images Directly 109 Finding Image Files 112 Conclusion 114 In and Out Introduction
85
107
117
117
Exporting Movies 118 Converting to Any Available Export Format 120 Converting to a Specific Export Format 121 Using Movie Export Settings 123 Importing Files 127 Filtering Out Movies 127 Importing In Place 129 Importing Files 131 Default Progress Functions 133 Custom Progress Functions 134 Opening the Dialog Box 137 Handling Progress Messages 138 Handling User Actions 1,$0 Closing the Dialog Box 143 Progress Functions for Image Operations 147 The Code 149 Conclusion 149
Chapter 6
Doug's 1st Movie
151
Introduction 151 The Structure of QuickTime Movies 152 The Structure of QuickTime Movie Files 154 Double-Fork and Single-Fork Movie Files 154 Fast Start Movie Files 156 Reference and Self-Contained Movie Files 157 Interleaved and Non-interleaved Movie Files 159 Creating QuickTime Movie Files 160 Creating a New Movie File 163 Adding Tracks to a Movie 164 Adding a Media to a Track 164 Adding Samples to a Media 166 Inserting a Media Segment 167 Adding the Movie Atom 168 Finishing Up 168 Adding Media Samples 169 Drawing Video Frames 170 Compressing Video Frames 173 Adding Video Frames to a Media 176 Saving a Movie 179 The Code 179 Conclusion 181
Contents
vii
Chapter 7
183
The Informant Introduction 183 Movie Posters 186 Getting and Setting Movie Poster Times 187 Working with Movie Poster Tracks 189 Movie Previews 191 Defining Movie Previews 191 Playing Movie Previews 193 Clearing Movie Previews 194 File Previews 196 Accessing File Previews 197 Creating File Previews 198 Movie Annotations 204 Creating the Edit Annotation Dialog Box 206 Showing the Current Annotation 208 Retrieving the Edited Annotation 210 Conclusion 214
Chapter 8
Chapter 9
The Atomic Caf~ Introduction 215 File Previews: The Sequel 216 Removing Existing Previews 217 Finding and Counting Atoms 219 Finding the Preview Data Atom 223 Removing and Freeing Atoms 224 Shortcut Movie Files 226 Atom Containers 229 Creating Atom Containers 231 Finding Atoms in Atom Containers 233 Getting Atom Data 233 Internet Connection Speed 234 Movie Tracks 237 Adding a Movie Track to a Movie 238 Creating a Movie Track Media Sample 240 The Code 243 Conclusion 244
215
Somewhere I'll Find You
245
Introduction
245
Data Handler Overview
viii
Contents
246
The File Data Handler 248 Opening a Movie File 249 Creating a Reference Movie File 250 The Handle Data Handler 254 The Resource Data Handler 257 The URL Data Handler 258 The Pointer Data Handler 261 File Transfer 262 Creating the Local File 263 Opening and Configuring Data Handlers Transferring Data Synchronously 264 Transferring Data Asynchronously 265 Tasking the Data Handlers 268 Finishing Up 269 Data Reference Extensions 273 Conclusion 276
Chapter 10
264
Word Is Out Introduction 277 The Edit Menu Revisited 281 Emulating QuickTime Player 283 Getting the Modifier Keys on Windows 285 Renaming the Edit Menu Items on Windows Putting It All Together 289 Text Importing 291 Importing Text from the Clipboard 291 Importing Text from a File 292 Text Tracks 294 Adding Text Media Samples 294 Positioning a Text Track 297 Enabling or Disabling a Text Track 298 Creating a Text Track 298 Text Searching 302 Text Editing 306 Getting the Current Text 307 Finding Sample Boundaries 308 Chapter Tracks 310 Hypertext Reference Tracks 314 Some Loose Ends 315 Conclusion 318
277
287
Contents
ix
Chapter 11
319
Timecode
Introduction 319 Timecode Standards 321 Timecode in QuickTime 323 Timecode Tracks 324 Creating a Timecode Sample Description 325 Creating a Timecode Media Sample 328 Setting the Timecode Track Geometry 330 Creating a Track Reference 332 Timecode Track Operations 336 Getting Information about a Timecode Track 337 Showing and Hiding a Timecode Track 338 Deleting a Timecode Track 340 Conclusion 342
Chapter 12
Chapter 13
x
Contents
2001: A Space Odyssey Introduction 343 Endian Issues 344 Working with Movie User Data 345 Reading and Writing Resource Data 347 The QuickTime Media Layer 350 Avoiding Namespace Collisions 351 Working with Files 352 Working with Resources 354 Working with Modal Dialog Boxes 355 Working with Modeless Dialog Boxes 361 Handling Strings 363 Converting Data Types 366 Handling Text 367 Carbon 367 Accessing Fields of Data Structures 368 Replacing Unsupported Functions 369 Working with Universal Procedure Pointers Conclusion 372 Honey, I Shrunk the Kids Introduction 373 Compression 375 Compressing Images 377 Getting the Image Pixel Map
343
371
373
377
Setting the Test Image 378 Installing Extended Procedures 379 Compressing the Image 383 Restricting Compressor Types 386 Compressing Image Sequences 386 Getting the Image Sequence 387 Configuring the Standard Image Compression Dialog Component Setting the Test Image 390 Displaying the Compression Settings Dialog Box 391 Adjusting the Sample Count 392 Creating the Target Movie 392 Compressing the Image Sequence 394 Finishing Up 396 Asynchronous Compression 397 Setting Up for Asynchronous Compression 398 Performing Asynchronous Compression 399 Weighing the Benefits 401 Conclusion 401 Chapter 14
Chapter 15
A Goofy Movie Introduction 403 Sprite Properties 406 Sprite Tracks 407 The Format of Key Frame Samples 40"7 The Format of Override Samples 409 Creating Sprite Tracks 409 Creating Sprite Tracks and Media 410 Setting Sprite Properties 413 Setting Sprite Images 419 Adding the Key Frame Sample to the Sprite Media Creating Override Samples 423 Setting Sprite Track Properties 424 Putting It All Together 426 Hit Testing 430 Conclusion 434
An Extremely Goofy Movie Introduction 435 Video Override Tracks 436 Building a Sprite Track 437 Adding a Video Track 439
389
403
421
435
Contents
xi
Adding a Track Reference 441 Setting the Input Map 442 Tweening 447 Graphics Mode Tweening 449 Adding a ]~veen Track 450 Setting the Input Map 453 Matrix 1k~reening 455 Building the ~veen Data Atom 455 Setting the Yween Offset and Duration Spin Tweening 457 Multimatrix Tweens 459 Conclusion 462 Chapter 16
xii
Contents
456
Wired Introduction 463 Events, Parameters, Actions 466 Specifying Events 466 Specifying Actions 468 Specifying Parameters 470 Controlling Movies Using Wired Sprites 470 Building the Sprite Track 470 Wiring Actions to Sprites 473 Setting the Track Properties 475 Setting the Track Layer 477 Setting the Movie Controller 478 Setting the Track Matrix 479 Putting It All Together 481 Wired Sprite Utilities 482 Adding Event and Action Atoms 483 Setting a Sprite Image Index 486 Variables and Conditionals 487 Setting Variables 487 Controlling Action Processing 488 Using Expressions and Operands 490 Draggable Sprites 491 Creating a Condition 492 Specifying the Conditional Expression 493 Specifying the Conditional Actions 494 Putting It All Together Again 495
463
Sprite Button Behaviors Conclusion 501
Chapter 17
Chapter 18
498
Moving Target Introduction 503 Targets 504 Targeting a Sprite 504 Targeting a Track 506 Movie-to-Movie Communication 511 Adding External Movie Targets 511 Specifying Movie Target Names and IDs 512 Retrieving Movie Target Names and IDs 516 Finding Movie Targets 518 Controlling External Movies 522 Retrieving a Movie's Own Target Name and ID Operand Targets 526 Movie-in-Movie Communication 530 Specifying Movie-in-Movie Targets 530 Using Movie-Loaded Events 531 Conclusion 532
503
523
Back In Action Introduction 533 Text Actions 535 Adding Actions to a Text Sample 536 Creating Text Actions 539 Creating Hypertext Actions 547 Key Events 549 Bouncing Sprites 552 Moving the Sprite 553 Detecting Track Rectangle Collisions 556 Colliding Sprites 560 Conclusion 563
533
Glossary
565
Index
587
About the CD
619
Contents
xiii
This Page Intentionally Left Blank
Preface
QuickTime is Apple's powerful and elegant software architecture for creating and playing back multimedia content on Macintosh and Windows computers. QuickTime is also my muse. Her p o w e r and her elegance inspired me several years ago to undertake to develop a useful tutorial on how to harness that power. I w a n t e d to chart an informative and reasonably comprehensive path through the QuickTime application p r o g r a m m i n g interfaces, investigating the v a r i o u s types of m e d i a s u p p o r t e d by Q u i c k T i m e and explaining how to integrate QuickTime movie playback into an application. This book is the fruit of those efforts. Or rather, this book is one of the fruits of those efforts, for the single book I planned to write has now grown into several books. This first book, QuickTime Toolkit, Volume One: Basic Movie Playback and Media Types, begins our journey into QuickTime programming. It shows how to use QuickTime functions to open and display a movie in a window on the screen. It also shows how to perform basic editing operations on a movie and how to save an edited movie into a file. [And that's just the first chapter!) This book shows how to work with a variety of media types, including video, still images, text, timecode, and sprites. It introduces concepts that are fundamental to QuickTime programming: movies, tracks, media, time scales, track references, atoms, atom containers, data references, media samples, sample descriptions, and a host of others. This first book ends with an in-depth look at one of the cornerstones of QuickTime interactivity: wired actions and wired sprites. The second book in this series, Q uickTime Toolkit, Volume Two: Advanced Movie Playback and Media Types, continues this journey by looking at a handful of the more advanced media types supported by QuickTime: video effects, skins, Flash, and QuickTime VR. It shows how to capture movies from sound and video input sources, broadcast movies to the Internet or a LAN, play movies full screen, and load movies asynchronously. That book ends with an important second look at data references and a first look at media sample references. Together, these two books present a detailed narrative that covers a substantial amount of what's involved in QuickTime application programming on both Macintosh and Windows computers.
Preface
xv
Development Platforms Did I mention that we'll be working with QuickTime on both Macintosh and W i n d o w s c o m p u t e r s ? From the first page to the last, I take cross-platform coding issues very seriously. Q u i c k T i m e w a s originally d e v e l o p e d on the Mac, but it has been r u n n i n g quite splendidly on W i n d o w s operating syst e m s from Q u i c k T i m e v e r s i o n 3.0 o n w a r d - - t h a t is, for well over half its existence. Virtually everything we can do w i t h QuickTime on the Mac we can also do on Windows, with very little additional programming effort. This c o m m i t m e n t to multiple platforms affects this book in some fundamental ways. The most significant influence concerns our choice of programming language and development tools. It would no doubt have been nice to rely on the services of a robust rapid application development e n v i r o n m e n t like Cocoa or PowerPlant or REALbasic or Visual Basic or Revolution. These tools abstract away most of the low-level details of constructing an application's user interface and handling user actions; this would allow us to focus more closely on the QuickTime-specific code that we need to write. But none of these RAD tools is sufficiently cross-platform or currently exposes enough of the QuickTime APIs to serve our needs. Accordingly, we'll use good old C as our primary programming language, and we'll use the standard platformspecific APIs to manage the windows, menus, dialog boxes, events, and messages of the applications we build. For instance, on Macintosh computers, we'll call NewCWindow to create a w i n d o w that contains a movie, while on Windows computers we'll call CreateWindowEx. To get the ball rolling, I've developed an application called QTShell that will form the basis of most of our work in this book and the next. We'll spend the first few chapters investigating how QTShell is put together and how we can use it to hold QuickTime code that executes on both target platforms. At first glance, the Macintosh version of QTShell may seem a bit quaint since it begins life using pre-Carbon functions like StandardGetFilePreview and StandardPutFile. (And some of the screenshots will have that oh-so-retro Mac OS 9 appearance!) This decision was deliberate and taken for a n u m b e r of reasons. The main reason is simply that Carbon-style interfaces like NavGetFi l e and NavPutFi l e have not yet migrated to Windows. I felt it would be best to begin our journey into QuickTime p r o g r a m m i n g with the simplest possible code that compiles and executes on both W i n d o w s and Macintosh computers. Not to worry, however: QTShell will gradually evolve during this journey, and by the middle of this first book it will be fully Carbonized and ready to run natively on Mac OS X. By the end of the second book, QTShell will use all the latest and greatest QuickTime and Carbon APIs.
xvi
Preface
How to Read This Book This book was written especially for software developers with little or no experience working with the QuickTime application p r o g r a m m i n g interfaces. It assumes that you know how to program on Windows or Macintosh computers, and it assumes that you are familiar with QuickTime as a multimedia technology. I firmly believe that if you read this book and its sequel in order, from beginning to end, you will acquire the knowledge and skills necessary to develop professional-grade applications that can create, display, and modify QuickTime content. At the same time, I am also convinced that these books are a significant resource for programmers who are already experienced in developing with QuickTime. I know this because I find myself continually referring to the sample applications described in these books, refreshing my memory on the particular techniques they employ for handling QuickTime movies or working with specific media types. The especial attention I give to novice QuickTime developers is manifested more in the order of explanation--the progressive disclosure of concepts--than in any limited depth of discussion. There should be plenty of useful information in these pages, sprinkled with the occasional insider tips and bits of otherwise undocumented technical details, to keep even the more experienced QuickTime programmers happy. One final point: I am of the opinion that QuickTime APIs are best studied in their natural habitat, namely, as part of functions that are designed to provide real-world application solutions. Each chapter dissects portions of one or more existing applications (whose complete source code is contained on the accompanying CDI. You should expect to encounter lots of useful code snippets along the way.
Preface xvii
Acknowledgements These books grew out of a series of articles published over the last four years in MacTech magazine. I am indebted to the staff at MacTech for giving a sustained voice to Q u i c k T i m e in their publication; t h a n k s are due to Nick DeMello, Eric Gundrum, Dave Mark, and especially to Jessica Stubblefield and Neil Ticktin. My colleagues at Apple, particularly in the QuickTime engineering group, have contributed in countless ways to my understanding of QuickTime and her foibles. A number of them have also directly influenced this book, either by reviewing portions of it or by providing sample code or sample content. I wish I could name them individually. I also wish I could thank by name those tireless colleagues in Apple's developer relations and technical publications groups with w h o m I have worked over the years. You guys rock! It is a pleasure to thank the team at Morgan Kaufmann who worked so hard to bring these books to print in amazingly short order. Special thanks are due to Elisabeth Beller, Richard Camp, and Tim Cox. I'd also like to thank Yonie Overton, Jennifer McClain, and Nancy Logan for their contributions. Finally, and not least, I should recognize Nathan and Raisa for their patience and support throughout the time I was writing these articles and books.
xviii Acknowledgements
It All Starts Today
introduction In this chapter, we'll learn how to open and display QuickTime movies and how to manage the user's interactions with those movies. This is a relatively simple task, and it's one that involves adding a fairly small a m o u n t of code to a basic working application. Keep in mind, however, that we want to support both the Mac OS and the major flavors of the Windows operating system (to wit: Windows 98, Windows NT, Windows ME, Windows 2000, and Windows XP). So we'll spend some time seeing how to open and play back movies using code that can run on both major platforms. Writing QuickTime code that is compatible with multiple platforms really isn't so hard. Indeed, part of the reason that QuickTime runs so well on Windows is that a good n u m b e r of the Macintosh programming concepts (including handles, resources, and file system specifications) were implemented on Windows as part of the QuickTime Media Layer (QTML). The hardest part of getting our QuickTime code to run on Windows may simply be creating a basic application shell or framework to hold that code. Accordingly, we'll spend a good bit of time in this chapter discussing just that issue.
M o v i e Controllers Before we start looking at our source code, however, let's take a minute to make clear what it is that we want to achieve. For the moment, we'll be content to build an application that can open and display QuickTime movies in windows on the screen. The Windows version of our basic application will have a frame window that contains a m e n u bar and within which we can open and display one or more movie windows. Figure 1.1 shows the appearance of the application's frame w i n d o w before the user has opened any QuickTime movies.
Figure 1.1
The frame window of the Windows application.
A movie w i n d o w will contain all the standard w i n d o w parts, the movie itself, and a special set of controls called the movie controller bar. Figure 1.2 shows a typical Macintosh movie window, and Figure 1.3 shows a Windows version of the same movie window. Both of these windows show the standard movie controller bar along the bottom edge of the window. The movie controller bar allows the user to control the playback of the movie and to navigate within it. For instance, the user can hold down the Frame Forward button to step the movie forward one frame at a time. Or, the user can drag the position t h u m b to set the current location in the movie. Some kinds of QuickTime movies use a different movie controller bar. For instance, QuickTime VR movies are not typically played frame by frame in a linear fashion. For these movies, you'll see the movie controller bar shown in Figure 1.4, which contains controls that allow the user to zoom in and out and to perform other operations on the movie. The movie controller bar is created and managed by a software component called a movie controller component (or, more briefly, a movie controller). Now here's the really fun part: once you've opened a QuickTime movie (using a few simple QuickTime functions), you can call a couple more functions to create and attach a movie controller to your movie. Thereafter, the
2
Chapter 1 It All Starts Today
Figure 1.2 A movie window on the Macintosh.
Figure 1.3 A movie window on Windows.
Movie Controllers
3
Figure 1,4 The movie controller bar for a QuickTime VR movie.
movie controller (and not your application} draws the movie controller bar and manages all events associated with the movie. Your application doesn't need to know how to jump to a new location in the movie or how to start and stop the movie playback. It simply needs to pass any events it receives to the movie controller component before acting on them itself. The movie controller intercepts any events that apply to it and reacts to them appropriately. So, the first lesson we need to take to heart is this: we can get all the basic movie playback capabilities simply by creating a movie controller, attaching it to our movie, and then giving it the first shot at handling any events we receive. And as if that weren't enough, the movie controller also provides an extremely easy way for us to perform basic editing operations on movies that support them. (Not all movies support cutting, copying, or pasting of movie segments; for instance, QuickTime VR movies do not.}
The Application Framework Now it's time for a bit of a detour. QuickTime provides an extensive set of services for handling digital media like sound, video, sprite animation, and the like. But of course we'll need to use other services to handle the basic graphical user interface for our QuickTime-savvy application--that is, its windows, menus, dialog boxes, and so forth. If you're an experienced Macintosh programmer, you're already familiar with the ideas underlying eventdriven programming on the Macintosh. Windows uses a slightly different
4
Chapter 1 It All Starts Today
idiom, sending messages to specific windows in an application. Since we want to support QuickTime programming on both Macintosh and Windows systems, we'll need to address separately the issues specific to each operating system, while trying to factor out as much code as possible to share between the two systems. Our general approach will go like this: we'll create two files, MacFramework. c and WinFramework. c, that handle the basic application services that are specific to the Macintosh and Windows operating systems, respectively. These services include starting up and shutting down the application, handling events and messages, creating windows, opening files dropped onto the application icon, and so forth. We won't delve very much into these framework files in this chapter, since there isn't very much in them of interest to QuickTime programmers. Suffice it to say that the Macintosh framework would look very familiar to anyone who cut his or her Mac programming eyeteeth in the late 1980s or 1990s; it uses standard event-driven programming techniques to handle the user's actions. And the Windows framework is a very straightforward implementation of the multiple document interface [MDI) specification defined by Microsoft for creating and managing one or more document windows within a general frame window. What's nice about MacFramework.c and WinFramework.c is that they have been carefully designed to call functions defined in a third file, ComFramework.c, for most QuickTime services or other services that are not systemspecific. ComFramework.c also defines a number of functions that are substantially the same on both platforms but that may require several short platform-dependent blocks (introduced by the compiler flags TARGET OS MAC and TARGET OS_WIN32). Keep in mind that (in this chapter, at least) we want to support only the most basic playback and editing of QuickTime movies, which is exactly what is provided by the basic framework. In future chapters, however, we'll want to add other capabilities to our applications. For instance, we'll want to handle some new menus in addition to the standard File and Edit menus, and we'll want to perform some application-specific tasks at idle time (perhaps change the pan angle of a QuickTime VR movie). To make it easy to add such capabilities, we create yet another file, called ComApplication.c, which defines a number of functions that are called at particular times by the basic framework. For instance, after the framework does any necessary menu adjusting for the File and Edit menus (enabling certain menu items and disabling others), it calls the function QTApp_AdjustMenus, defined in ComAppl i cation.c, to allow us to adjust any application-specific menus. Since we don't have any application-specific tasks to perform, for the moment at least, we can ignore ComApplication.c and instead turn our attention to the file ComFramework.c.
The Application Framework
5
Handling Movie Windows To get a taste for how our basic f r a m e w o r k works, let's begin by considering how we w a n t to m a n a g e o u r application's movie windows. On the Macintosh, a movie w i n d o w is of type WindowPtr; on Windows, a movie w i n d o w is of type HWND.To simplify the code that handles movie windows, we define a custom type that refers to either a Macintosh movie w i n d o w or a W i n d o w s movie window, like this" #if TARGETOS MAC typedef WindowPtr #endif #if TARGETOS WIN32 typedef HWND #endif m
WindowReference;
m
WindowReference;
We need to maintain some information for each movie w i n d o w displayed by our application. We'll use the standard technique of defining a structure to hold this information and allocating an instance of that structure for each open movie window. Let's call this instance a window object record. typedef struct { WindowReference fWi ndow; Movie fMovie; MovieControl ler fControl ler; FSSpec fFi I eFSSpec; short fFileResID; short fFi I eRefNum; Boolean fCanResi zeWindow; Boolean flsDirty; Boolean f l sQTVRMovie; QTVRInstance flnstance; OSType fObj ectType; Handle fAppData; } WindowObjectRecord, *WindowObjectPtr, **WindowObject;
Notice that the first field of this structure, fWindow, is of type WindowReference, which (as we've just seen) is a WindowPtr on the Mac and an HWNDon Windows. The fMovie and fControl l er fields identify the movie and movie controller. The next three fields maintain information about the location of the movie file on disk and in memory. The three fields after that indicate w h e t h e r the movie w i n d o w can be resized (which is almost always true), w h e t h e r the movie data has changed since it was opened or last saved, and
6
Chapter 1 It All Starts Today
w h e t h e r the movie is a QuickTime VR movie. If the movie is a Q u i c k T i m e VR movie, the fInstance field holds a special identifier associated with the movie. The f0bjectType field holds an arbitrary identifier that is u n i q u e to our application; we use this field just to make sure that w e ' v e got a valid w i n d o w object. Finally, the fAppData field holds a handle to any applicationspecific data. For now, we w o n ' t need to use this field. W h e n the user selects a movie file to open, we need to allocate a w i n d o w object record and attach it to the w i n d o w in w h i c h the movie is opened. The standard Macintosh w a y to do this is to use the SetWRefCon function to set the w i n d o w ' s reference constant, an application-specific 32-bit value, to the handle to the w i n d o w object record. W i n d o w s provides a similar capability to attach an application-specific 32-bit value to a window, w i t h the SetWindowLong function. Listing 1.1 shows the code we use for creating a window object.
Listing 1.1 Creating a window object. void QTFrame CreateWindowObject (WindowReference theWindow)
{
WindowObject
myWindowObject= NULL;
i f (theWindow == NULL) return; / / allocate space for a window object record and f i l l in some of i t s fields myWindowObject = (Wi ndowObject) NewHandI eCl ear (s i zeof (Wi ndowObject Record) ) ; i f (myWindowObject ! = NULL) { (**myWindowObject).fWindow = theWindow; (**myWindowObject) .fControl ler = NULL; (**myWindowObject) .fObjectType = kMovieControl lerObject; (**myWindowObject).flnstance = NULL; (**myWindowObject).fCanResizeWindow = true; (**myWi ndowObject), f l sDi rty = false; (**myWi ndowObject), fAppData = NULL;
/ / associate myWindowObject (which may be NULL) with the window # i f TARGET OS MAC SetWRefCon(theWindow, (long)myWindowObject) ; #endif # i f TARGET OS WIN32 SetWindowLong(theWindow, GWL USERDATA, (LPARAM)myWindowObject); B
/ / associate a GrafPort with this window CreatePortAssociation(theWindow, NULL, OL); #endif
The Application Framework
7
/ / set the current port to the new window MacSetPort (QTFrame GetPortFromWindowReference(theWi ndow)) ;
Internally, QuickTime does some of its drawing using QuickDraw, the collection of system software routines that perform graphic operations on the user's screen (and elsewhere). And, as you know, QuickDraw does all of its drawing within the current graphics port. On the Macintosh, there is a very close connection between a WindowPtr and a graphics port, but there is no such connection between Windows HWNDsand graphics ports. So, w h e n our application is running on Windows, we need to call the CreatePortAssociation function to create a connection between the HWNDand a graphics port (of type GrafPtr). Once we've called CreatePortAssociation to associate a graphics port with an HWND,we can subsequently call the GetNativeWindowPort function to get a
pointer to the graphics port that is associated with that window. Listing 1.2 defines a function that we can call from either Macintosh or Windows code to get a window's graphics port. (Now you can understand the last line in Listing 1.1, which sets the current graphics port to the port associated with the specified window.l Listing 1.2: Getting the graphics port associated with a window. GrafPtr QTFrame GetPortFromWindowReference (WindowReference theWindow)
{
m
#if TARGETOS MAC return((GrafPtr) GetWindowPort(theWi ndow)); #endif #if TARGETOS WIN32 return (GetNati veWindowPort(theWi ndow)) ; #endif m
}
Let's look briefly at a few other small utilities that we'll use extensively in our framework code. Keep in mind that our general goal here is to provide utilities that insulate us from the specific details of any particular operating system. One thing we'll need to do fairly often is retrieve the windowspecific data that we've stored in the window object record associated with a given movie window. We can use the OTFrame_GetWindowObjectFromWindow function, defined in Listing 1.3, to do this. Aside from a few sanity checks (namely, the calls to QTFrame_IsAppWindow and QTFrame_IsWindowObjectOurs), this function is essentially the reverse of attaching the window object record to a window.
8
Chapter 1 It All Starts Today
Listing 1.3 Getting the window-specific data. WindowObject QTFrame_GetWindowObjectFromWi ndow (Wi ndowReference theWindow)
{
WindowObject
myWindowObject= NULL;
i f (!QTFrame_IsAppWindow(theWindow)) return(NULL) ; #if TARGETOS MAC myWindowObject = (Wi ndowObject) GetWRefCon(theWi ndow) ; #endif #if TARGETOS WIN32 myWindowObject = (WindowObject)GetWindowLong(theWindow, GWL USERDATA); #endif / / make sure this is a window object i f ( ! QTFrame_IsWi ndowObjectOurs (myWindowObject) ) return (NULL) ; return (myWindowObject) ;
Finally, we'll often need to iterate through all open movie windows. On Windows, this is fairly simple since the operating system provides an easy way for us to ask for the first (or next) child of the MDI frame window, which we know to be a movie window. On the Macintosh, it's a bit harder since we need to skip over any dialog windows or other types of w i n d o w s that might be in our w i n d o w list. We can use the functions QTFrame GetFrontAppWindow and QTFrame_GetNextAppWindow, defined in Listings 1.4 and 1.5, to step through all open windows that belong to our application.
Listing 1.4 Getting the first application window. WindowReference QTFrame_GetFrontAppWindow (void)
{
#if TARGETOS MAC return (FrontWi ndow() ) ; #endif #if TARGETOS WIN32 return(GetWindow(ghWnd, GWHWNDFIRST)); #endif m
}
The Application Framework
9
One thing to notice in Listing 1.5 is that we did not find the next w i n d o w by reading the nextWi ndow field of the w i n d o w record, as used to be standard practice. Instead, we've used the accessor function GetNextWindow defined in the header file MacWindows.h. Here we're treating the w i n d o w record as an opaque data structure and thereby facilitating our eventual move to Carboncompatible APIs. [For more on Carbon, see page 113.)
Listing 1.5 Getting the next application window. WindowReference QTFrame GetNextAppWindow (Wi ndowReference theWindow)
{
# i f TARGET OS MAC return(theWindow == NULL ? NULL : GetNextWindow(theWindow)); #endi f # i f TARGET OS WIN32 return(GetWindow(theWindow, GW HWNDNEXT)); #endi f
}
To find the front movie w i n d o w on the Macintosh, we'll just walk through the w i n d o w list until we find the first w i n d o w with a non-NULL w i n d o w object attached to it, as shown in Listing 1.6.
Listing 1.6 Getting the first movie window. WindowReference QTFrame GetFrontMovieWindow (void)
{
WindowReference
myWindow;
# i f TARGET OS MAC myWindow = QTFrame_GetFrontAppWindow() ; while ((myWindow ! = NULL) && (QTFrame_GetWindowObjectFromWindow(myWindow) == NULL)) myWindow = QTFrame_GetNextAppWindow(myWindow) ; #endi f # i f TARGET OS WIN32 myWindow = (HWND)SendMessage(ghWndMDIClient, WMMDIGETACTIVE, O, OL); #endif m
return (myWindow) ;
10
Chapter 1 It All Starts Today
And to get the movie w i n d o w that follows a specific movie window, w e ' l l continue walking t h r o u g h the w i n d o w list until w e find the next w i n d o w w i t h a non-NULL w i n d o w object, as s h o w n in Listing 1.7.
Listing 1.7 Getting the next movie window. WindowReference QTFrame_GetNextMovieWi ndow (Wi ndowReference theWi ndow)
{
WindowReference
myWindow;
# i f TARGET OS MAC myWindow = QTFrame_GetNextAppWindow(theWi ndow) ; while ((myWindow ! = NULL) && (QTFrame_GetWindowObjectFromWindow(myWindow) == NULL)) myWindow = QTFrame GetNextAppWindow(myWindow) ; #endif m
# i f TARGET OS WIN32 myWindow = GetWindow(theWindow, GW HWNDNEXT); #endif return (myWindow) ;
Handling Menus Now we w a n t to do the s a m e thing for m e n u s that w e ' v e done for w i n d o w s , namely, develop a unified w a y to refer to m e n u s and m e n u items, so that we can (for instance) enable and disable m e n u items or process the user's selection of m e n u items in a platform-neutral m a n n e r . Happily, this is a simpler task than the one we just solved. On the Macintosh, a particular m e n u item is specified using two pieces of information, the m e n u ID and the index of the i t e m in the specified m e n u . On Windows, a m e n u item is specified by a single, 16obit " m e n u item identifier," w h i c h is an arbitrary value that we associate w i t h the m e n u item. Because the value on W i n d o w s is arbitrary, we'll construct it by setting the high-order 8 bits to the Macintosh m e n u ID and the low-order 8 bits to the index of the m e n u item in the m e n u . Suppose we use the following values for the m e n u bar and m e n u resource IDs in our Macintosh resource file: #define #define #define #define
kMenuBarResID kAppleMenuResID kFi leMenuResID kEditMenuResID
128 128 129 130
The Application Framework
11
Then we can define our m e n u item identifiers like this: #define #define #define #define #define #define #define
IDS IDM IDM IDM IDM IDM IDM
FILEMENU FILENEW FILEOPEN FILECLOSE FILESAVE FILESAVEAS EXIT
33024 33025 33026 33027 33028 33029 33031
/ / (kFi leMenuResID<<8)+O / / (kFi I eMenuResID<<8)+1
#define #define #define #define #define #define #define
IDS EDITMENU IDM EDITUNDO IDM EDITCUT IDM EDITCOPY IDM EDITPASTE IDM EDITCLEAR IDM EDITSELECTALL
33280 33281 33283 33284 33285 33286 33288
/ / (kEdi tMenuResI D<<8)+0 / / (kEdi tMenuResID<<8)+1
You might be wondering w h y we didn't just define, for instance, IBS_FILEMENU as (kFileMenuResID<<8)+0. In fact, this works fine w h e n developing either Mac or Windows applications using CodeWarrior, but it doesn't seem to work w h e n using Microsoft Developer Studio on Windows machines. Go figure--literally! Also, for those of you with calculators that don't have a "<<" key, bit-shifting left by 8 is the same as multiplying by 256. In effect, w e ' r e simply adopting the Windows method of specifying m e n u items, but doing so in a m a n n e r that allows us to retrieve the Macintosh m e n u ID and m e n u item index from the arbitrary m e n u item identifier, using these macros: #define MENUIDENTIFIER(menuID,menultem) #define MENU ID(menuldentifier) #define MENU ITEM(menuIdentifier) m
( (menuI D<<8)+(menuItem) ) ( (menuIdenti fi er&OxffO0)>>8) ( (menuIdenti fi er&OxOOff) )
Now we need to devise a way of referring to m e n u s themselves in a crossplatform way. On the Macintosh, we access m e n u s using variables and parameters of type MenuHandle, while on Windows, we use variables and parameters of type HMENU.Once again, we'll define a custom type that refers to either a Macintosh m e n u or a Windows menu, like this: #if TARGETOS MAC typedef MenuHandle #endif #if TARGET OS WIN32 typedef HMENU #endif
12
Chapter 1 It All Starts Today
MenuReference;
MenuReference;
Let's see how this all fits together in practice. Suppose we w a n t to enable or disable a particular m e n u item. For instance, the user m a y have edited a movie window, in which case we want to make sure that the Undo m e n u item in the Edit m e n u is enabled. We can use the function QTFrame SetMenuItemState defined in Listing 1.8. m
Listing 1.8 Enabling or disabling a menu item. void QTFrame_SetMenultemState (MenuReference theMenu, UInt16 theMenultem, short theState)
{
#if TARGET OS MAC i f (theState == kEnableMenuItem) EnableMenultem(theMenu, MENU ITEM(theMenultem)) ; else Di sabl eMenuI tem(theMenu, MENU_ITEM(theMenuItem) ) ; #endif #if TARGET OS WIN32 EnableMenultem(theMenu, (UINT)theMenultem, (UINT)theState); #endif n
}
On Windows, QTFrame_SetMenultemState uses the Windows function EnableMenuItem to set the specified m e n u item to the desired state. On the Macintosh, it calls either of the two Macintosh functions EnableMenuItem or Di sabl eMenuItem, depending on the value of theState.
QuickTime Support Now let's get back to the task at hand, w h i c h is showing how to open and display QuickTime movies and how to handle basic editing operations on those movies.
Opening a Movie File Let's suppose that w e ' v e already got a file s y s t e m specification record that indicates which movie file the user wants to open. We might have called the Standard File Package or (on Macintosh) the n e w e r Navigation Services to elicit a file from the user, or we might have gotten the file specification from an O p e n D o c u m e n t Apple event. At this point, we can call the f u n c t i o n OpenMovieFi le to open the movie file: myErr = OpenMovieFile(&myFSSpec, &myRefNum, fsRdWrPerm) ;
QuickTime Support
13
Next we need to load the movie data from the file. We can do this by calling the NewMovieFromFi l e function, as follows" myResID = O; myErr = NewMovieFromFile(&myMovie, myRefNum, &myResID, NULL, newMovieActive, NULL);
N o w we need to create a w i n d o w in w h i c h to display the movie. This operation is platform-specific" on the Macintosh, we can call the NewCWindow function, and on Windows, we can call the CreateWindowEx function. Once w e ' v e successfully created a window, we just need to pass that w i n d o w to the OTFrame_CreateWindowObject function defined in Listing 1.1, w h i c h (as we saw) allocates some m e m o r y to hold information n e e d e d by our f r a m e w o r k s and attaches the w i n d o w object record to the window.
A t t a c h i n g a M o v i e Controller So, at this point, w e ' v e o p e n e d the movie file and read the movie data from it. We've also created a w i n d o w {which is not yet visible), created a w i n d o w object, and attached the w i n d o w object to the window. Before we can m a k e the movie w i n d o w visible, we need to create a movie controller and attach it to the window. We do this by calling the f r a m e w o r k function OTFrame_SetupController" myMC = QTFrame_SetupController(myMovie, myWindow, true) ;
The QTFrame_SetupControl l er function is a tad lengthy, so we w o n ' t show it all here. But it does only four things of any real interest: it creates a new movie controller for the specified movie; it enables movie controller editing; it adds a grow box to the movie controller bar; and it installs a movie controller action filter function. We'll discuss movie editing a little later; for now, let's see how to do the other three tasks. Before we can create a movie controller, we need to d e t e r m i n e the rectangle within w h i c h the movie is going to be displayed. O u r basic application draws the movie at its natural size, w h i c h we can obtain by calling the GetMovieBox function (making sure that the top-left corner of the movie is at
0,0)" GetMovieBox(myMovie, &myRect); MacOffsetRect (&myRect, -myRect. 1eft, -myRect.top) ; SetMovi eBox(myMovie, &myRect);
14
Chapter 1 It All Starts Today
Then we can create a movie controller with this single line of code: myMC = NewMovieController(myMovie, &myRect, mcTopLeftMovie) ;
Even though we've opened the movie at its natural size, w e ' d like to allow the user to resize the movie window. By default, the movie controller does not provide this capability, so we need to do a little w o r k to enable movie resizing. The first thing we need to do is find the upper bounds for the movie w i n d o w size. In other words, we need to find the largest rectangle that can contain a movie window. We could set an arbitrary upper b o u n d for our movie w i n d o w size, but instead we'll allow the user to resize a movie to fit as m u c h of the available screen space as possible. On the Macintosh, we can get this area like this" gMCResi zeBounds = (**GetGrayRgn ()). rgnBBox;
(This line of code isn't Carbon-compliant, of course. We still have a little bit of w o r k to do before our M a c i n t o s h f r a m e w o r k is fully Carbonized.) O n Windows, we can call the GetOesktopWindow and GetWindowRect functions to get the size of the desktop. Note that GetWi ndowRect r e t u r n s a s t r u c t u r e of type RECT, w h i c h on W i n d o w s consists of four long integers. So we need to convert the RECTinto a Rect, like this: RECT
myRect;
GetWindowRect(GetDesktopWindow(), &myRect) ; OffsetRect (&myRect, -myRect. I eft, -myRect. top) ; gMCResizeBounds, top = (short)myRect. top; gMCResizeBounds.left = (short)myRect.left; gMCResi zeBounds, right = (short)myRect. r i g h t ; gMCResizeBounds, bottom = (short)myRect. bottom;
Then, we can enable w i n d o w resizing by calling the MCDoAction function with the mcActi onSetGrowBoxBounds parameter: MCDoActi on (myMC, mcActi onSetGrowBoxBounds, &gMCResi zeBounds) ; MCDoAction is a general-purpose tool for getting a movie controller to perform various actions. (If we take a look in Movies.h, a standard header file provided by QuickTime, we'll see well over 60 defined movie controller actions.) We'll use MCDoAction extensively throughout this book. For the moment, though, it's important to know that before the movie controller performs any action initiated by MCDoAction, it informs our application of the
QuickTime Support
15
pending action and, indeed, allows it to cancel that action. To get these notifications of pending actions, we need to install a movie controller action filter function, like this" MCSetActi onFi I terWi thRefCon (myMC, NewMCActi onFi I terWi t hRefConProc (QTApp_MCActi onFi I terProc), (l ong)myWindowObject) ;
The first p a r a m e t e r is the movie controller to w h i c h we want to attach a filter function. The second p a r a m e t e r is a universal procedure pointer f o r the filter function itself. The third p a r a m e t e r is an application-specific reference constant that is passed to the filter function w h e n e v e r it is called. As you can see, w e ' r e passing the w i n d o w object to the filter function so that we can gain access to the movie w i n d o w ' s data inside that function. Listing 1.9 shows a typical movie controller action filter function. This function handles only one action, mcActionControl lerSizeChanged, which the movie controller sends to our filter function w h e n e v e r the user has dragged the resize box in the controller bar. Our job is then to resize the associated movie window.
Listing 1.9 Intercepting movie controller actions. PASCAL_RTN Boolean QTApp_MCActionFilterProc (MovieController theMC, short theAction, void *theParams, long theRefCon)
{
#pragma unused(theMC, theParams) Boolean
WindowObject
i sHandl ed = fal se; myWindowObject = NULL;
myWindowObject = (Wi ndowObject)theRefCon; i f (myWindowObject == NULL) return (i sHandl ed) ; swi tch (theActi on) { / / handle window resizing case mcActionControl lerSizeChanged" QTFrame_SizeWindowToMovie (myWindowObject) ; break; default" break;
} return (i sHandled) ;
16
Chapter 1 It All Starts Today
A movie controller action filter function should r e t u r n fa] se as its function result if it w a n t s the movie controller to handle the action specified by the theAction parameter. Conversely, it should r e t u r n true if it d o e s n ' t w a n t the movie controller to handle the action. In the case of the mcActionControl]erSizeChanged action, we r e t u r n fa] se to let the controller do a n y processing it needs to.
Handling Events Once w e ' v e got a movie file o p e n e d in a w i n d o w and have attached a movie controller to it, we need to let the movie controller handle any events that apply to it. (For instance, if the user hits the space bar, the movie controller should start or stop the movie, d e p e n d i n g on its current playing state.) The main thing we need to do is pass the event to the MCIsPlayerEvent function, but how we do this differs b e t w e e n the Mac and W i n d o w s platforms. On Macintosh, we get events in our main event loop by calling Wai tNextEvent. W h e n we retrieve an event in this way, we d o n ' t k n o w to which, if any, movie controller it might apply. So, we'll just pass the event to all open movie controllers until we find one that accepts it. Listing 1.10 shows h o w we do this. Listing 1.10 Handling events on the Macintosh. static Boolean QTFrame_CheckMovieControllers (EventRecord *theEvent)
{
Wi ndowPtr WindowObject Movi eControl I er
myWindow; myWindowObject; myMC;
myWindow = QTFrame GetFrontMovieWindow(); while (myWindow != NULL) { myWindowObject = QTFrame GetWindowObjectFromWi ndow(myWindow) ; i f (myWindowObject ! = NULL) { myMC = (**myWindowObject).fController; i f (myMC ! = NULL) i f (MCIsPlayerEvent(myMC, theEvent)) return(true) ; n
m
}
myWindow = QTFrame_GetNextMovieWi ndow(myWindow) ; return(false);
QuickTime Support
17
On Windows, however, system and user actions are handled s o m e w h a t differently. Windows sends messages describing those actions directly to the w i n d o w procedure of the target window, so we k n o w in advance to w h i c h movie controller the action might apply. The only "gotcha" is that MClsPlayerEvent wants to get Macintosh-style events, not Windows-style messages. So we need to call the function WinEventToMacEvent to translate the Windows message into a Macintosh event, as shown in Listing 1.11.
Listing 1.11 Handling events on Windows. WinEventToMacEvent(&myMsg, &myMacEvent); / / pass the Mac event to the movie controller i f the movie window i s n ' t minimized i f (! Islconic(theWnd)) MCIsPI ayerEvent (myMC, (EventRecord *)&myMacEvent) ;
Editing a Movie Some QuickTime movie controllers support the typical cut, copy, paste, and clear editing operations t h r o u g h a handful of high-level functions that are very easy to use. By default, a newly created movie controller has editing turned off, so we need to explicitly turn it on, like this: MCEnableEditing(myMC, true) ;
If we're interested in finding out w h e t h e r a particular movie controller supports editing, we can inspect the value returned by MCEnableEditing, which is nonzero if the specified movie controller does not support editing. We'll ignore that return value here, since we'll dynamically determine whether a movie controller supports editing w h e n e v e r we adjust our application's menus. W h e n the user chooses an item in our Edit m e n u [or performs some equivalent keyboard operation), we simply need to call the corresponding movie controller function. For instance, if the user chooses the Cut m e n u item, we'll call the MCCut function and also set a flag to indicate that the movie's data has changed: myMovie = MCCut(myMC); (**myWindowObject).flsDirty : true;
MCCut removes the current movie selection from the movie associated with the specified movie controller. Moreover, MCCut returns the cut movie selection to us; this is useful if we want to allow the user to paste that seg-
18
Chapter 1 It All Starts Today
m e n t back into the same movie (or indeed into some other movie). We need to call the PutMovieOnScrap function if we w a n t to allow that segment to be pasted. Listing 1.12 shows our entire function for handling the Edit menu.
Listing 1.12 Handling items in the Edit menu. void QTFrame_HandleEditMenultem (WindowReference theWindow, UInt16 theMenultem)
{
WindowObject MovieControl ler Movie
myWindowObject = NULL; myMC = NULL; myMovie = NULL;
myWindowObject = QTFrame_GetWindowObjectFromWi ndow(theWi ndow) ; myMC = QTFrame GetMCFromWindow(theWi ndow) ; / / make sure we have a valid movie c o n t r o l l e r and a valid window object i f ((myMC == NULL) II (myWindowObject == NULL)) return; switch (theMenultem) { case IDM EDITUNDO: MCUndo(myMC); (**myWi ndowObject), f l sDi rty = case IDM EDITCUT: myMovie = MCCut(myMC); (**myWi ndowObject), f l sDi rty = case IDM EDITCOPY: myMovie = MCCopy(myMC); case IDM EDITPASTE: MCPaste(myMC, NULL); (**myWi ndowObject), f l sDi rty = case IDM EDITCLEAR: MCCIear (myMC) ; (**myWi ndowObject), f l sDi rty = case IDM EDITSELECTALL: QTUti I s SelectAl IMovie(myMC) ; default:
true;
break;
true;
break; break;
true;
break;
true;
break; break; break;
/ / place any cut or copied movie segment onto the global scrap i f (myMovie I = NULL) { PutMovieOnScrap(myMovie, OL) ; Di sposeMovi e (myMovi e).;
}
QuickTime Support
19
The movie controller also provides an easy w a y for us to d e t e r m i n e w h i c h Edit m e n u items need to be enabled or disabled at any time. The function MCfetContro] ]erInfo returns a 32-bit set of flags that we can inspect to see the current status of the movie controller, such as w h e t h e r the user has edited the movie. So, to enable or disable the U n d o m e n u c o m m a n d , we can use code like this: MCGetControl lerlnfo(myMC, &myFlags) ; i f (myFlags & mclnfoUndoAvailable) QTFrame_SetMenuItemState (myMenu, I DM_EDI TUNDO, kEnableMenuI tem) ; else QTFrame SetMenuItemState(myMenu, IDM EDITUNDO, kDi sabl eMenultem) ; See the function QTFrame_AdjustMenus in ComFramework.c for the complete story on adjusting all the Edit (and File) m e n u items.
Conclusion Well, w e ' v e accomplished w h a t we set out to do, namely, show how to open QuickTime movie files in movie w i n d o w s and handle user interactions with those movies. Along the way, we also took a fairly long look at some of the techniques we can use, here and in the future, to make sure that our code operates identically on both Macintosh and W i n d o w s operating systems. By spending the time now to u n d e r s t a n d how to use the p l a t f o r m - i n d e p e n d e n t utilities defined in ComFramework.c, we have (I hope) m a d e it easier to spend time later focussing on the QuickTime-specific tasks we w a n t to accomplish.
20
Chapter 1 It Aft Starts Today
Control
Introduction In the previous chapter, we learned how to open QuickTime movies and display them in windows on the screen. We also learned how to create movie controllers and pass events to them so that the user can interact with the movies in the standard ways. For normal "linear" QuickTime movies, these basic interactions include starting and stopping the movie, moving quickly forward or backward in the movie, adjusting the volume of the sound track, and performing simple editing operations on the movie. For QuickTime VR movies, the basic interactions include changing the pan and tilt angles, zooming in or out, moving from one node to another, and displaying the visible hotspots. All of this interaction is provided, with virtually no programming on our part, by the movie controller associated with the movie. In this chapter, we're going to continue working with movie controllers. Now that we've done all the work necessary to associate a movie controller with a movie and to draw the movie and movie controller bar in a w i n d o w on the screen, there is a tremendous amount of "low-hanging fruit" that we can pick with a very small amount of code. Here we'll see how to hide and show the movie controller bar, hide and show particular buttons in the bar, attach a pop-up m e n u to the custom button in the controller bar, and perform several other tasks on the controller bar. Toward the end of this chapter, I'll also throw in a few goodies that are only slightly more complicated, like getting a movie controller to open a URL in the user's default Web browser. Along the way, we'll take a look at movie user data, which is some custom data that can be attached to a movie. We can use a movie's user data to specify lots of information about a movie, including the initial position of the movie window, the movie's looping state, and the movie's copyright information. This isn't completely unrelated to our main topic in this chapter, because movie controllers and movie user data are linked in one important
21
way: for a movie to use a special movie controller (that is, any controller other than the standard linear movie controller), the movie's user data must include a piece of information that specifies which other controller to use. W h e n we call NewMovieController to associate a movie controller with an open movie, QuickTime looks for that piece of user data and, if it finds it, opens the specified movie controller. Here we'll see how we too can inspect that data and use it for our own purposes.
M a n a g i n g the Controller Bar Before we begin, it's worthwhile to emphasize once again the distinction between a movie controller and a movie controller bar. A movie controller is a software component that we can use to manage the user's interaction with a QuickTime movie. A movie controller bar is a visible set of controls and other user interface elements that is displayed by a movie controller (usually along the bottom of the movie window) and that provides some ways for the user to interact with the movie. The movie controller typically supports some forms of user interaction that are not associated with the controller bar (for instance, hitting the space bar starts or stops a linear QuickTime movie). Moreover, it's possible to have a movie controller associated with a movie but no visible movie controller bar (as we'll see shortly); even w h e n the movie controller bar is hidden, the user can still interact with the movie. This distinction needs emphasizing only because it's not u n c o m m o n to hear people talk about the movie controller w h e n they really m e a n the movie controller bar.
Showing and Hiding the Controller Bar So let's get started picking some of that low-hanging fruit. One of the simplest things we can do is hide the movie controller bar, with this single line of code" MCSetVisible(myMC, false) ;
(Here myMC refers to a movie controller.} The MCSetVisi bl e function sets the controller bar to be visible or invisible, according to the Boolean value we pass it. Figure 2.1 shows a movie w i n d o w with the controller bar hidden. Let's take a m o m e n t to define a couple of functions that call MCSetVisible with the appropriate parameter, mainly to give ourselves a more uniform naming convention and to make our code a bit more readable. Listing 2.1 defines the function QTUtils_HideControllerBar and Listing 2.2 defines the opposite function, QTUti 1s_ShowControl 1erBar.
22
Chapter2 Control
Figure 2.1 A movie window with a hidden movie controller bar. Listing 2.1
Hiding the controller bar.
void QTUtils HideControllerBar (MovieController theMC)
{
}
MCSetVisible(theMC, false) ;
Listing 2.2. Showing the controller bar.
void QTUtils ShowControllerBar (MovieController theMC)
{
}
m
MCSetVisible(theMC, true) ;
We can call the MCGetVisible function to determine w h e t h e r the controller bar is currently visible, as shown in Listing 2.3. Listing 2.3 Determining whether the controller bar is visible.
Boolean QTUtils IsControllerBarVisible (MovieController theMC)
{
}
m
return( (Boolean) MCGetVisi bl e (theMC)) ;
Managing the Controller Bar
23
And we can put this all together to define a function that toggles the current visibility state of the controller bar, as shown in Listing 2.4.
Listing 2.4 Toggling the visibility state of the controller bar. void QTUtiI s_ToggleControl lerBar (MovieControl ler theMC)
{
i f (QTUtils IsControllerBarVisible(theMC)) QTUti I s Hi deControl lerBar(theMC) ; else QTUti Is ShowControllerBar(theMC) ;
No doubt there are more than a few of you out there scratching your heads and wondering w h y we didn't simply define QTUti l s_ToggleControl l erBar with this single line of code" MCSetVisible(theMC, ! (Boolean)MCGetVisible(theMC)) ; We surely could have done that, but personally I find the definition in Listing 2.4 to be a tad more readable.
Using Badges While we're on the topic of hiding and showing the controller bar, it's w o r t h mentioning a useful but little-used user interface element k n o w n as a badge. The idea behind badges is that a movie w i n d o w that has no controller bar and whose movie is not actively playing looks remarkably like a w i n d o w containing a still picture. (Take another look at Figure 2.1.} To help the user realize that the w i n d o w is actually a movie w i n d o w containing a stopped movie with no visible controller bar, the movie controller can be made to display a badge, as seen in Figure 2.2. The movie controller displays the badge w h e n e v e r the movie is stopped and there is no visible movie controller bar, but only if we have previously configured the movie controller to do so, like this: MCDoAction(myMC, mcActionSetUseBadge, (void *)true); A useful feature of a badge is that the user can make the controller bar reappear by clicking in the badge. If we want, we can explicitly disable the movie controller from using a badge by executing this line of code: MCDoAction(myMC, mcActionSetUseBadge, (void *)false);
24
Chapter 2 Control
Figure 2.2 A movie window displaying a badge.
You'll find that line in the function QTFrame SetupControl l er in our source code file ComAppl i c a t i o n . c to s u p p r e s s badge display for any m o v i e s we display.
Attaching and Detaching the Controller Bar The default behavior of a movie controller is to draw the movie controller bar contiguous to, and just underneath, the movie that it is controlling. (The movie controller is said to be attached to the movie.) In virtually all cases, this results in exactly the appearance for movie w i n d o w s that users expect. But for some purposes, it might be preferable to position the movie controller bar elsewhere. Q u i c k T i m e allows us to do this quite easily. The first thing we need to do is detach the movie controller from the movie. We can do this by calling MCSetControl lerAttached like this: MCSetControl lerAttached(myMC, false) ;
The next thing we need to do is specify the desired new locations of the movie and movie controller bar. Listing 2.5 defines a function, OTUtils_PutControl l erBarOnTop, that places the controller bar above the movie.
Managing the Controller Bar
25
Listing 2.5 Putting the controller bar above a movie. void QTUtils PutControllerBarOnTop (MovieController theMC)
{
i f (theMC == NULL) return; i f (MCIsControllerAttached(theMC) == 1) { Rect myMCRect; Rect myMovieRect; MCGetControl I erBoundsRect (theMC, &myMCRect); myMovieRect = myMCRect; myMCRect.bottom = myMCRect,top + QTUti I s_GetControl I erBarHei ght (theMC) ; myMovieRect, top = myMCRect,bottom + 1; MCSetControl lerAttached(theMC, false) ; MCPositionController(theMC, &myMovieRect, &myMCRect, OL);
Figure 2.3 shows a sample movie w i n d o w after the QTUti l s_PutControll erBarOnTop function has been called on it. In this case, the controller bar is still contiguous to the movie, but it is no longer attached to it. In general, as I've said, it's best to retain the standard appearance of movie windows, with the controller underneath and attached to the window. It is, however, sometimes useful to detach a movie controller from its movie, even if just briefly. Suppose we want to determine the height of the movie controller bar (which, incidentally, we needed to do in Listing 2.5). There is no programming interface for getting this information directly. QuickTime does, however, supply the function MCGetControllerBoundsRect, which returns the rectangle that encloses the movie controller. The only "gotcha" here is that the movie controller rectangle is defined as the rectangle that encloses the movie controller bar and the movie, if the movie controller is attached to the movie; otherwise, if the movie controller is detached from the movie, the movie controller rectangle encloses just the movie controller bar. So, to determine the height of the movie controller bar, we can first detach the controller bar, if it's currently attached, and then retrieve the movie controller rectangle. Listing 2.6 shows our complete function for doing this.
26
Chapter 2 Control
Figure 2.3 A controller bar on top of a movie. Listing 2..6 Finding the height of the controller bar. short QTUti I s GetControl lerBarHeight (MovieControl ler theMC)
{
m
Boolean Rect
wasAttached = false; myRect;
/ / i f the controller bar is attached, detach i t (and remember we did so) i f (MCIsControllerAttached(theMC) == 1) { wasAttached = true; MCSetControl lerAttached(theMC, false) ;
}
/ / get the rectangle of the controller MCGetControl lerBoundsRect (theMC, &myRect); / / now reattach the controller bar, i f i t was o r i g i n a l l y attached i f (wasAttached) MCSetControllerAttached(theMC, true) ; return(myRect.bottom - myRect.top) ;
Managing the Controller Bar
27
Managing Controller Bar Buttons Now that we've played a little bit with the movie controller bar, let's consider how to work with individual buttons in the controller bar. In general, the only thing we can do with the individual buttons is hide and show them. We cannot disable or enable them, and we cannot directly handle clicks on them. The only exception to this concerns the controller bar's custom button; we can intercept and react to clicks on the custom button, as we'll see later in this chapter.
Managing the Standard Controller Buttons Let's begin by considering the standard QuickTime movie controller bar. There are only four buttons that QuickTime allows us to show or hide: the Volume control (sometimes also called the Speaker button), the Step Forward and Step Backward buttons, and the custom button. By default, the Volume control is displayed if and only if the associated movie contains a sound or music track. We can hide the Volume control, if we choose, but we cannot make it visible if the movie does not have a sound or music track. All the other buttons are displayed all the time, unless we programmatically suppress them. We can suppress the display of some of these buttons by manipulating the movie controller's control flags, a 32-bit value whose bits encode the settings of various movie display and playback options. QuickTime defines these constants for showing and hiding buttons in the standard movie controller bar: enum { mcFlagSuppressStepButtons mcFlagSuppressSpeakerButton mcFlagsUseCustomButton
};
=1<<1, = 1<<2,
=1<<5
So, for instance, we can hide the two Step buttons by executing this code: MCDoAction(myMC, mcActionGetFlags, &myControllerFlags) ; MCDoAction(myMC, mcActionSetFlags, (void *) (myControllerFlags I mcFlagSuppressStepButtons));
The idea here is simple: we get the current control flags, set the bit in those flags that suppresses display of the Step buttons, and then send the updated flags back to the movie controller. Naturally, to show a button that's been hidden, we do the opposite; namely, we get the current control flags, clear the appropriate bit in those flags, and then send the updated flags back to the movie controller:
28
Chapter2 Control
MCDoActi on (myMC, mcActi onGetFl ags, &myControlI erFl ags) ; MCDoAction (myMC, mcActionSetFl ags, (void *) (myControl lerFlags & -mcFlagSuppressStepButtons)) ;
One thing to watch out for is that the semantics for the custom button are the reverse of those for the other buttons: setting the flag for the custom button results in that button being displayed, while clearing that flag results in the button being hidden. This is because the custom button is, by default, not displayed.
Managing the QuickTime VR Controller Buttons The QuickTime VR movie controller is more generous than the standard movie controller in that it allows us to show or hide any of the controls in its controller bar (including the Volume control). But this generosity comes at the price of a slight increase in complexity. The simple clearing and setting of bits in the control flags just demonstrated does not always give the desired result. This is because the QuickTime VR movie controller sometimes suppresses buttons even w h e n those buttons have not been explicitly suppressed in the control flags. For example, if a particular QuickTime VR movie does not contain a sound track, then the movie controller automatically suppresses the Volume control. Likewise, if a QuickTime VR movie does contain a sound track, then the Volume control is automatically displayed, again without regard to the actual value of the mcFlagSuppressSpeakerButton flag in the control flags. So far, this behavior is identical to that of the standard movie controller. The main difference between the two controllers is that the QuickTime VR movie controller does provide a way for us to override its default behavior. We might want to do this, for instance, if our application has loaded a sound from a resource or some external file and we'd like to allow the user to adjust the volume of that sound using the Volume control. (Wait a second! Didn't I just say that we can't intercept mouse clicks on the controller bar buttons? If so, then how are we supposed to respond to the user's adjusting the volume using the Volume control? The answer is that we can't directly determine that the user has clicked the Volume control, but we can wait for the movie controller to tell us that the user has changed the volume by looking for movie controller actions of type mcActionSetVolume. So, indirectly, we can accomplish what we want here.) To let us override its default behavior, the QuickTime VR movie controller maintains two sets of flags, a set of control flags (with which we are already familiar) and a set of explicit flags. The explicit flags indicate which bits in the control flags are to be used explicitly (that is, overriding any default behaviors of the movie controller). If a particular bit in the explicit
Managing Controller Bar Buttons
29
flags is set, then the corresponding bit in the control flags is interpreted as the desired setting for that feature. Conversely, if a particular bit in the explicit flags is clear, then the corresponding bit in the control flags is interpreted as it normally is. In this way, the explicit flags operate as a sort of mask for the control bits. To make this clearer, let's consider a concrete example. The QuickTime VR movie controller defines these constants for working with its controller bar buttons: enum { mcFlagQTVRSuppressBackBtn mcFlagQTVRSuppressZoomBtns mcFlagQTVRSuppressHotSpotBtn mcFl agQTVRSuppressTranslateBtn mcFlagQTVRSuppressHelpText mcFlagQTVRSuppressHotSpotNames mcFl agQTVRExpli ci tFl agSet
};
= I L << 1 6 , = I L << 1 7 , = I L << 1 8 , = I L << 1 9 , = I L << 2 0 , = I L << 2 1 , = 1L << 31
If, for example, bit 17 is set in a movie's explicit flags and bit 17 is clear in that movie's control flags, then the zoom buttons are displayed (that is, they are not suppressed). Similarly, if bit 2 is set in a movie's explicit flags and bit 2 is clear in that movie's control flags, then the volume control is displayed, whether or not the movie contains a sound track. There is one final element to this whole story. We need a way of getting and setting a movie controller's explicit flags. Rather than introduce any new movie controller actions, the QuickTime VR engineers decided to use the existing actions (mcActionfietFlags and mcActionSetFlags) but to have those actions operate on the explicit flags or the control flags, depending upon the setting of bit 31 (defined as mcFlagQTVRExplicitFlagSet) in the value passed to MCOoAction. To get or set a bit in a movie's explicit flags, we must set the flag mcF] agQIVRExp] i ci tF] agSet in the parameter we pass to mcActi onGetF] ags or mcActionSetF] ags. To get or set a bit in a movie's control flags, we must clear the flag mcFlagQTVRExplicitFlagSet in the parameter we pass to mcActi onGetFl ags or mcActi onSetFl ags. So let's put this all together and see how we can force the Volume control to be displayed in the controller bar of a QuickTime VR movie that does not contain a sound track. First, we need to get the movie's explicit flags and set the bit in those explicit flags that corresponds to the Volume control: myControl lerFlags = mcFlagQTVRExplicitFlagSet; MCDoActi on (myMC, mcActi onGetFl ags, &myControlI erFl ags) ; MCDoAction(myMC, mcActionSetFlags, (void *) ((myControllerFlags I mcFlagSuppressSpeakerButton) I mcFlagQTVRExplicitFlagSet)) ;
310
Chapter2 Control
Note that w h e n we use the defined constants to set values in the explicit flags, the constant names might be a bit confusing. For instance, setting the bit mcFlagSuppressSpeakerButton in a movie's explicit flags doesn't cause the speaker to be suppressed; it just means: "use the actual value of the mcF1agSuppressSpeakerButton bit in the control flags." Next, we need to set the appropriate bit in the movie's control flags, like this: myControllerFlags = O; MCDoActi on (myMC, mcActi onGetFl ags, &myControlI erFl ags) ; MCDoAction(myMC, mcActionSetFlags, (void *) (myControllerFlags & ~mcFlagSuppressSpeakerButton & -mcFlagQTVRExpli ci tFl agSet)) ;
Once we've executed these six lines of code, the Volume control will appear in the controller bar, w h e t h e r or not the associated QuickTime VR movie has a sound track. Listing 2.7 combines the results of our discussions in this section and the previous section into a single, unified function that we can use to show buttons in a movie controller bar.
Listing 2.7
Showing a button in the controller bar.
void QTUtils ShowControllerButton (MovieController theMC, long theButton)
{
m
l ong
myControl I erFl ags;
/ / handle the custom button separately i f (theButton == mcFlagsUseCustomButton) { MCDoActi on (theMC, mcActi onGetFl ags, &myControlI erFl ags) ; MCDoAction(theMC, mcActionSetFlags, (void *) (myControllerFlags I theButton)) ; } else { / / get the current explicit flags and set the explicit flag for the specified button myControllerFlags = mcFlagQTVRExplicitFlagSet; MCDoAction(theMC, mcActionGetFlags, &myControllerFlags) ; MCDoAction(theMC, mcActionSetFlags, (void *)((myControllerFlags I theButton) I mcFlagQTVRExplicitFlagSet)); / / get the current control flags and clear the suppress flag for the specified button myControllerFlags = O; MCDoAction(theMC, mcActionGetFlags, &myControllerFlags) ; MCDoAction(theMC, mcActionSetFlags, (void *) (myControllerFlags & ~theButton & ~mcFlagQTVRExplicitFlagSet)) ;
Managing Controller Bar Buttons
31
The corresponding function to hide a button in the movie controller bar is exactly parallel; if you're interested, take a look at OIUtils_HideControllerButton in the file OTUti I i t i es. c. Currently, only the QuickTime VR movie controller maintains a separate set of explicit flags and hence knows h o w to interpret the mcF] agQTVRExp] i ci tFlagSet flag. This means that the standard QuickTime movie controller will actually be getting and setting the movie's control flags w h e n we make the calls to get and set the explicit flags. I've written QIUtils_ShowControl lerButton and QTUtils HideControllerButton so that no harm is done if a movie controller does not support any explicit flags.
Using the Controller Bar Custom Button In QuickTime 3, movie controllers gained the ability to display and manage a custom controller bar button. As you can see in Figure 2.4, the custom button is displayed on the right side of the controller bar, just to the left of the grow box.
Figure 2.4 The controller bar's custom button. The button contains a downward-pointing triangle, which is suggestive of the triangle contained in a standard pop-up menu. This is largely because the main intended use of the custom button was to allow the QuickTime plug-in for Web browsers to display a pop-up m e n u of commands. Figure 2.5 shows a typical QuickTime movie embedded in a Web page, with the popup m e n u popped up. The QuickTime public headers and libraries contain everything that we need to put this button to work in our own applications. Let's see how to do that. The first thing we need to do is display the custom button in the controller bar that's attached to our movies. We can use the function QTUtils ShowControllerButton (defined in Listing 2.7) with the constant mcFlagsUseCustomButton to show the custom button, like this: QTUti I s ShowControl I erButton (mcFlagsUseCustomButton) ;
Then, all we need to do is intercept the user's clicks on the custom button and react accordingly. This is a task for our movie controller action filter function. We can add these lines to our ever expanding switch statement in the filter function QXApp_MCActionFilterProc:
32
Chapter 2 Control
case mcActionCustomButtonClick: QTCustom HandleCustomButtonClick(theMC, (EventRecord *)theParams, theRefCon) ; break; So, when the function QTCustom_HandleCustomButtonClick gets called, we know that the user has clicked the custom button in the controller bar. The theParams p a r a m e t e r to our movie controller action filter function points to an event record describing that click. As you can see, w e ' v e cast the theParams p a r a m e t e r to a pointer to an event record and passed it to our clickhandling function. We can now call the M e n u Manager function PopUpMenuSel ect to display a pop-up m e n u at the point specified in the event record. PopUpMenuSelect is very m u c h like the function MenuSelect in that it takes care of drawing the m e n u on the screen, tracking mouse movements within the menu, and then returning to our application an indication of which item (if any) in the m e n u was selected. If you've written any Macintosh applications, you are probably already familiar with MenuSelect, since you've almost certainly used it to handle your application's pull-down m e n u s in the Macintosh m e n u bar. But you might never have used PopUpMenuSelect, since it's called internally by the Control Manager w h e n you handle user actions on pop-up m e n u controls. At any rate, it's just the function we need here.
Figure 2.5 The pop-up menu of the QuickTime browser plug-in.
Using the Controller Bar Custom Button
33
Before we can call PopUpMenuSelect, however, we need to do a little work. First, the point contained in the event record passed to our action filter function is in coordinates that are local to the w i n d o w associated with the movie controller. PopUpMenuSelect wants its input to be in global screen coordinates. So we need to call the LocalToGlobal function to convert from one coordinate system to the other. Second, and more important, we need to get a handle to the m e n u that we want to be popped up at that location on the screen. We can do this in several ways. We could just read the m e n u from a resource file (using the MacGetMenu function), or we could build the m e n u programmatically on the fly (using the NewMenu and AppendMenu functions). To make things easy for the moment, we'll use the latter method and build the m e n u from scratch. The complete QTCustom_HandleCustomButtonClick function is shown in Listing 2.8. Listing 2.8
Handling clicks on the custom button in the controller bar.
void QTCustom HandleCustomButtonClick (MovieController theMC, EventRecord *theEvent, l ong theRefCon)
{
#pragma unused(theMC) MenuHandle Wi ndowObject StringPtr StringPtr StringPtr StringPtr
myMenu = NULL; myWindowObject = NULL; myMenuTitle = QTUtils ConvertCToPascalString(kMenuTitle) myltemlText = QTUtils ConvertCToPascaIString(kltemlText) myltem2Text = QTUtils ConvertCToPascaIString(kltem2Text) myltem3Text = QTUtils ConvertCToPascaIString(kltem3Text)
myWindowObject = (Wi ndowObject) theRefCon; i f (myWindowObject == NULL) goto bai I ; / / make sure we got a valid event i f (theEvent == NULL) goto bai I ; / / create a new menu myMenu = NewMenu(kCustomButtonMenuID, myMenuTitle) ; i f (myMenu ! = NULL) { long myltem = O; Point myPoint; / / add some items to the menu MacAppendMenu(myMenu, myI temlText) ; MacAppendMenu(myMenu, myI tem2Text) ; MacAppendMenu(myMenu, myI tem3Text) ;
34
Chapter 2 Control
; ; ; ;
/ / insert the menu into the menu l i s t MaclnsertMenu(myMenu, hierMenu) ; / / by default, MacAppendMenu enables the item; / / do any desired menu item disabling here i f (! (**myWindowObject).flsDirty) Di sabl eMenuItem(myMenu, kSaveltemlndex) ; / / find the location of the mouse click; / / the top-left corner of the pop-up menu is anchored at this point myPoint = theEvent->where; LocaIToGl obal (&myPoint) ; / / display the pop-up menu and handle the item selected myltem = PopUpMenuSelect(myMenu, myPoint.v, myPoint.h, myltem); switch (MENU_ITEM(myltem)) { case kl temllndex: QTFrame_Beep() ; break; case kl tem2Index. QTFrame_ShowAboutBox() ; break; case kl tem31ndex. QTFrame_UpdateMovieFi I e ( (**myWi ndowObject). fWi ndow) ; break; / / remove the menu from the menu l i s t MacDeleteMenu((**myMenu) .menuID) ; / / dispose of the menu Di sposeMenu(myMenu) ; bail: free (myMenuTi t I e) ; free (myI temlText) ; free (myI tem2Text) ; free (myI tem3Text) ;
}
Figure 2.6 shows the pop-up menu displayed by a call to QTCustom_HandleCustomButtonCl i ck.
Now keep in mind that this is all being accomplished using APIs that belong to QuickTime and to the Macintosh Menu Manager. But the code
Using the Controller Bar Custom Button
35
Figure 2.6 Our application's custom pop-up menu.
we've developed compiles, links, and executes just fine under Windows as well. I find this just simply amazing (but maybe I'm easy to amaze). At the very least, this is one more example of the excellent work done by the engineers who designed and implemented the QuickTime Media Layer (QTML) on Windows, which provides support under Windows for the parts of the Macintosh Operating System and User Interface Toolbox that are used by QuickTime.
S e l e c t i n g an E n t i r e M o v i e You might have noticed, when we were discussing movie editing in the previous chapter, that there are movie controller functions for handling all the standard menu items in the Edit menu, except for the Select All item. To handle that item, we simply called the application-defined function OTUtils SelectAllMovie, which we did not discuss further. It's time to do so now. The linear movie controller supports two actions that are useful here, mcActionSetSelectionBegin and mcActionSetSelectionDuration. To select the
entire movie, as you've probably guessed, we can use mcActi onSetSel ect i onBegin to set the beginning of the movie selection to the beginning of the movie, and then we can use mcActionSetSelectionDuration to set the selection duration to the duration of the entire movie.
The only issue left to consider, then, is how to specify times in a movie. Time management is generally a big concern for multimedia content [as should be clear from the emphasis on time in the product name "Quick-
36
Chapter 2 Control
Time"}. At the most basic level, robust time services are necessary to get a video track to play back at the right speed no matter what the processing speed of the user's computer. They are also necessary to maintain a precise synchronization between sound and video tracks in a movie. QuickTime provides a number of time-related services, so time is something that we'll need to discuss more than once in this book. For the moment, we'll look only at what we need in order to solve the problem at hand. The mcActi onSetSel ect i onBegi n and mcActi onSetSel ect i onOurat i on controller actions both take a parameter of type TimeRecord, which is declared like
this: struct TimeRecord { CompTimeValue TimeScale TimeBase
};
value; scale; base;
A movie's time scale is the number of units that elapse every second. Figuratively speaking, the time scale is the ruler by which the movie is measured. A movie's time scale is set at the time the movie is created and can be determined programmatically using the fietMovieTimeScal e function. Like the distance between markings on a ruler, the number we use for a time scale is somewhat arbitrary. A standard value is 600, which permits nonfractional values for most of the common movie rates {for example, a movie playing at 30 frames per second has 20 units per frame, if the time scale is 600). The values we specify in the value field are interpreted relative to the time scale specified in the scale field. The CompTimeValue structure contains two fields, hi and l o, which specify (respectively) the high- and low-order 32-bits of our time value. To specify the beginning of the movie, we need to set both of these fields to 0. To specify the desired duration of the movie, we can set the lo field to the value returned by the GetMovieDuration function. Luckily, GetMovieDuration returns the length of the specified movie in units relative to the movie's time scale. (The base field is not used here, so we'll set it to 0.) Listing 2.9 defines the function QTUti 1s_SelectA11Movie.
Listing 2.9 Selecting an entire movie. OSErr QTUtiIs SelectAl IMovie (MovieController theMC)
{
TimeRecord Movie ComponentResult
myTimeRecord; myMovie = NULL; myErr = noErr;
i f (theMC : : NULL) return (paramErr) ;
Selecting an Entire Movie
37
myMovie = MCGetMovie(theMC) ; i f (myMovie == NULL) return (paramErr) ; myTimeRecord.value.hi = O; myTimeRecord.value.lo = O; myTimeRecord, base = O; myTimeRecord.scale = GetMovieTimeScale(myMovie) ; myErr = MCDoAction(theMC, mcActionSetSelectionBegin, &myTimeRecord); i f (myErr != noErr) return((OSErr)myErr) ; myTimeRecord.value.hi : O; myTimeRecord.value. Io = GetMovieDuration(myMovie) ; myTimeRecord, base = O; myTimeRecord.scale = GetMovieTimeScale(myMovie) ; myErr = MCDoAction(theMC, mcActionSetSelectionDuration, &myTimeRecord) ; return ((OSErr)myErr) ;
0
Movie User Data A QuickTime movie file contains a list of data, called movie user data, which we can read and manipulate. This list consists of individual items, each of which has an associated type. Some types of user data are predefined by QuickTime, such as the movie's name, copyright information, or author; other types can be defined by specific applications and used for their own purposes. For instance, if we wanted to store the geographical location of a QuickTime VR panorama, we could attach to the movie a piece of movie user data specifying the location's latitude and longitude. A user data item type is a four-character code that looks rather like a resource type. For instance, the movie name user data item has the type ' 9 and the movie copyright user data item has the type ' 9 In addition, there can be more than one user data item of a particular type. If so, those items are distinguished from one another by their index [which starts at 1). To retrieve a movie's user data item of a specific type and index, we first need to call the GetMovieUserData function to get the entire user data list from the movie. GetMovieUserData returns a reference to the user data list; we can pass this reference to any of several other functions that retrieve individual items from the list. For instance, if we know that the item contains text data,
38
Chapter2 Control
we can pass the user data reference to the GetUserDataText function to get that text data. Or, we can pass t h a t reference to the GetUserDataItem function to get any kind of data (including text data) from the user data list. By convention, the predefined types of movie user data are always stored in a bigendian format, so we need to r e m e m b e r to convert the data from bigoendian format to the nativeoendian format w h e n we read the user data; we also need to convert the data from the nativeoendian format to bigoendian format w h e n we write the user data. (See Chapter 12, "2001: A Space Odyssey" for a detailed account of big-endian and nativeoendian data formats.)
Specifying the Controller Type As m e n t i o n e d earlier, any Q u i c k T i m e movie that uses a special movie controller needs to contain a user data item that specifies w h i c h movie controller to use. The type of this user data item is kUserDataMovieControl l erType (defined in the header file Movies.h as 'ctyp'). Listing 2.10 defines the function QTUti I s_GetControl l erType, w h i c h returns the controller type specified in a m o v i e ' s user data, or kUnknownType if no controller type is specified in that user data. Listing 2.10 Getting a movie's controller type. OSType QTUtils GetControllerType (Movie theMovie)
{
UserData OSType OSErr
myUserData= NULL; myType = kUnknownType; myErr = noErr;
/ / make sure we've got a movie i f (theMovie == NULL) return (myType) ; myUserData = GetMovieUserData(theMovie) ; i f (myUserData l= NULL) { myErr = GetUserDataltem(myUserData, &myType, sizeof(myType), kUserDataMovieControl lerType, O) ; i f (myErr == noErr) myType = EndianU32 BtoN(myType);
}
a
return (myType) ;
Movie User Data
39
QTUti 1s_GetControl 1erType just assembles the pieces we've talked about: it calls GetMovieUserData to get the movie's user data list; then it calls GetUserDataItem to retrieve the movie controller type user data item; finally, it converts the big-endian user data to native-endian data using the macro Endi anU32 BtoN. Let's look at a sample use of the QTUtils_GetControl lerType function. Our
basic application framework needs to know whether a movie uses the QuickTime VR movie controller, since the QuickTime VR movie controller does a lot of its own cursor management. When our application starts up, and after it has opened the movie file, it calls the QTUtils_IsQTVRMovie function defined in Listing 2.11 to determine whether the movie is a QuickTime VR movie. As you can see, QTUtils_IsQTVRMovie calls QTUtils_GetControl lerType to determine the movie's controller type and then compares that type against the controller types that we know are used by QuickTime VR movies. (kQTVRQTVRTypeis the controller type of all version 2.0 and later QuickTime VR files, while kQTVRO1dObjectType and kQTVROldPanoType are controller types used by version 1.0 object and panorama movies.)
Listing 2.11
Determining if a movie is a QuickTime VR movie.
Boolean QTUtils IsQTVRMovie (Movie theMovie)
{
Boolean OSType
mylsQTVRMovie = false; myType;
/ / QTVR movies have a special piece of user data identifying the movie controller type myType = QTUti I s GetControl lerType(theMovie) ; i f ((myType == kQTVRQTVRType)II (myType == kQTVROldPanoType)II (myType == kQTVROldObjectType)) mylsQTVRMovie = true; return (myI sQTVRMovi e) ;
It's really just as easy to set a movie's controller type as it is to get a movie's controller type. Instead of calling GetUserOataItem, we need to call SetUserOataItem, as shown in Listing 2.12. Notice that we make sure to convert the type passed to us into big-endian format.
40
Chapter 2 Control
Listing 2.12. Setting a movie's controller type. OSErr QTUtils SetControllerType (Movie theMovie, OSType theType)
{
UserData OSErr
myUserData; myErr = noErr;
/ / make sure we've got a movie i f (theMovie == NULL) return (paramErr) ; / / get the movie's user data l i s t myUserData = GetMovieUserData(theMovie) ; i f (myUserData == NULL) return (paramErr) ; theType = EndianU32 NtoB(theType); myErr = SetUserDataItem(myUserData, &theType, sizeof(theType), kUserDataMovieControl lerType, O) ; m
return (myErr) ;
There are a couple of points to keep in mind here, however. First, the SetUserDataItem function changes only the copy of the movie's user data that's in m e m o r y (and to which myUserData is a reference). If we also want to change the user data stored in the movie file, we need to update the movie file (usually by calling the UpdateMovieResource function). If we were to call QTUti 1s_SetControl lerType to change the movie's controller type and then immediately closed the movie file, the new controller type would not be stored in the movie file. Second, calling QTUti l s_SetControl 1erType does not change the movie controller associated with an open movie, unless we call it before we call NewMovieController. The reason, of course, is that NewMovieController looks at
the movie's user data at the time we call it; any subsequent changes to the movie user data do not automatically change the movie controller assigned to a movie. It occasionally happens that we do want to change a movie's controller type on the fly. To do this, we need to change the movie's user data, close the existing movie controller, and then open a new movie controller. We can accomplish all this by calling the OTUtils_ChangeControllerType function defined in Listing 2.13.
Movie User Data
4'11
Listing 2.13 Changing a movie's controller type dynamically. MovieControl ler QTUtil s ChangeControl lerType (MovieControl ler theMC, OSType theType, long theFl ags) MovieController Movie Rect OSErr
myMC = NULL; myMovie = NULL; myRect; myErr = noErr;
/ / make sure we've got a movie controller i f (theMC =- NULL) return(NULL); / / get the movie associated with that controller myMovie = MCGetMovie(theMC) ; i f (myMovie == NULL) return (NULL) ; GetMovieBox(myMovie, &myRect) ; / / set the new controller type in the movie's user data l i s t myErr = QTUtils SetControllerType(myMovie, theType) ; i f (myErr ! = noErr) return(NULL); / / dispose of the existing controller Di sposeMovi eContro I I er (theMC) ; / / create a new controller of the specified type myMC = NewMovieController(myMovie, &myRect, theFlags) ; return (myMC);
You'll notice that QTUti I s_ChangeControl lerType takes a movie controller, and not a movie, as an input parameter. The reason for this is that we need to use both the movie and the original movie controller within the function and that we can always get the movie currently associated with a movie controller by calling the MCGetMovie function. There is no easy w a y to go in the reverse direction and obtain the movie controller [if any I currently associated with a movie.
42
Chapter 2 Control
M a n i p u l a t i n g a Movie's Looping State Another standard use for movie user data is to specify a movie's looping state. There are three possibilities here. First, a movie can play forward from beginning to end and then stop; a movie like this is said to have no looping. Second, a movie can play forward from beginning to end, and then return to the beginning and play forward again, and so on; a movie like this is said to have normal looping. Finally, a movie can play forward from beginning to end and then play backwards from end to beginning, and then play forward from beginning to end, and so on; a movie like this is said to have palindrome looping. (Go hang a salami; I'm a lasagna hog!) A movie's looping state is specified by a user data item of type 'LOOP'. If the movie doesn't contain an item of this type, then we'll assume that its looping state is no looping. If it does contain an item of this type, then the item data (a long integer) is 0 for normal looping and 1 for palindrome looping. To make our work more readable, let's define three looping state constants: enum { kNormal Loopi ng kPal i ndromeLoopi ng kNoLooping
};
=0, = 1~
=2
Now we can define (in Listing 2.14) a function QTUtils_GetMovieFileLoopingInfo that returns one of these constants, depending on whether it finds a movie user data item of type 'LOOP' and (if there is one) on what the data in that item is. Listing 2.14 Getting a movie's looping state. OSErr QTUtils GetMovieFileLoopinglnfo (Movie theMovie, long *theLooplnfo)
{
UserData long OSErr
myUserData= NULL; myLoopInfo = kNoLooping; myErr = paramErr;
/ / make sure we've got a movie i f (theMovie := NULL) goto bai I ;
Movie User Data
43
/ / get the movie's user data l i s t myUserData = GetMovieUserData(theMovie) ; i f (myUserData l= NULL) { myErr = GetUserDataltem(myUserData, &myLooplnfo, sizeof(myLooplnfo), FOUR CHARCODE('LOOP'), O) ; i f (myErr == noErr) myLooplnfo = EndianS32 BtoN(myLooplnfo);
}
bail: *theLooplnfo = myLooplnfo; return (myErr) ;
W h e n our application opens a movie, it would be nice to set its looping state to the state indicated in its 'LOOP' movie user data item (or indicated by the absence of that item). Happily, the movie controller supports two actions, mcActionSetLooping and mcActionSetLooplsPalindrome, that we can issue to set the looping state to the correct state. The mcActionSetLooping action enables looping of any variety, and the mcActionSetLooplsPalindrome action enables palindrome looping. Listing 2.15 defines a function that we can use to read the looping information from the file and set the appropriate looping state on the specified movie. Listing 2.15 Setting a movie's looping state. OSErr QTUtils SetLoopingStateFromFile (Movie theMovie, MovieController theMC)
{
long OSErr
myLooplnfo = kNoLooping; myErr = noErr;
myErr = QTUti I s GetMovieFileLoopinglnfo(theMovie, &myLooplnfo) ; switch (myLooplnfo) { u
case kNormalLoopi ng: MCDoAction(theMC, mcActionSetLooping, (void *)true); MCDoAction(theMC, mcActionSetLooplsPal indrome, (void *) false) ; break; case kPal indromeLooping: MCDoAction(theMC, mcActionSetLooping, (void *)true); MCDoAction(theMC, mcActionSetLooplsPalindrome, (void *)true) ; break;
414
Chapter 2 Control
case kNoLooping: default: MCDoAction(theMC, mcActionSetLooping, (void *)false); MCDoAction(theMC, mcActionSetLooplsPalindrome, (void *)false) ; break; return (myErr) ;
The final thing we might want to do is store a movie's current looping state in the movie file. We've already seen how to set a movie's controller type (see Listing 2.12), so we might expect to set the looping state in roughly the same manner. However, we cannot just add a 'LOOP' movie user data item with the appropriate data, since the no-looping state is indicated by the absence of such a user data item. What we need to do, therefore, is first remove any existing user data items of type 'LOOP'; then, for normal looping or palindrome looping, we can add a user data item of the appropriate type. It's easy enough to remove all existing items of type 'LOOP" just keep removing the first such item until there are no more remaining, like this: myCount = CountUserDataType(myUserData, FOURCHAR CODE('LOOP')) ; while (myCount--) RemoveUserData(myUserData, FOURCHAR CODE('LOOP'), 1) ; D
m
The complete function for setting a movie's looping state is shown in Listing 2.16. Listing 2.16
Setting the looping state of a movie file.
OSErr QTUtils SetMovieFileLoopinglnfo (Movie theMovie, long theLooplnfo)
{
m
UserData long short OSErr
myUserData= NULL; myLooplnfo; myCount = O; myErr = paramErr;
/ / get the movie's user data myUserData = GetMovieUserData(theMovie) ; i f (myUserData == NULL) goto bai I ;
Movie User Data
45
/ / we want to end up with at most one user data item of type 'LOOP', / / so l e t ' s remove any existing ones myCount = CountUserDataType(myUserData, FOUR_CHAR_CODE('LOOP')); while (myCount--) RemoveUserData(myUserData, FOURCHAR CODE('LOOP'), 1); / / make sure we're writing big-endian data myLooplnfo = EndianU32_NtoB(theLoopInfo) ; switch (theLooplnfo) { case kNormalLooping: case kPal indromeLooping: myErr = SetUserDataltem(myUserData, &myLooplnfo, sizeof(long), FOUR CHARCODE( 'LOOP'), O) ; break; m
n
case kNoLooping" default" myErr = noErr; break; bailreturn (myErr) ;
}
Once again, w e ' d need to make sure to call UpdateMovieResource to update the user data in the movie file or our changes to the user data will be lost w h e n we close the movie.
G e t t i n g a M o v i e ' s S t o r e d W i n d o w Position To finish off our adventures with a movie's user data, let's consider briefly how to get the stored screen position of a movie window. A movie's user data list can contain an item of type 'WLOC', whose accompanying data consists of a 32-bit value that is interpreted as a point that specifies the position of the upper-left corner of the movie window. Listing 2.17 shows the function QTUtils GetWindowPositionFromFile, whose implementation should be completely familiar to you by now.
4G
Chapter2 Control
Listing 2.'17 Getting a movie's stored screen position. OSErr QTUtils GetWindowPositionFromFile (Movie theMovie, Point *thePoint)
{
UserData Point OSErr
myUserData= NULL; myPoint = {kDefaultWindowX, kDefaultWindowY}; myErr = paramErr;
/ / make sure we've got a movie i f (theMovie == NULL) goto bai I ; / / get the movie's user data l i s t myUserData = GetMovieUserData (theMovi e) ; i f (myUserData ! = NULL) { myErr = GetUserDataltem(myUserData, &myPoint, sizeof(Point), FOUR CHAR CODE('WLOC'), O) ; i f (myErr == noErr) { myPoint.v = EndianS16 BtoN(myPoint.v); myPoint.h = EndianS16 BtoN(myPoint.h); m
}
}
m
bail: *thePoint = myPoint; return (myErr) ;
}
In theory, before opening the movie at the window position returned by QTUtils_GetWindowPositionFromFile, we should verify that that position is reasonable for the computer on which the movie is being opened. Otherwise, the new movie window might not be visible. This sanity check is left as an exercise for the reader. Keep in mind that we can look for any of the predefined types of movie user data, and we can define our own types. We'd retrieve our own custom types of user data items in exactly the same way that we've retrieved the types defined by QuickTime. The only restriction is that Apple has reserved for itself all four-character types that consist solely of lowercase letters and special characters. So, for instance, we shouldn't use ' b i l l ' as a custom user data type, but we're free to use 'BILL' or 'Bill '. I know what you're thinking: what about 'WLOC'and 'LOOP'? Shouldn't they instead be 'wloc' and 'loop', to fit within Apple's reserved types? Indeed, they should be, if they were predefined user data types. But they aren't; we
Movie User Data
47
w o n ' t find 'WLOCi or 'LOOP' defined in any QuickTime header file. Rather, these types were used as custom types early on in QuickTime's history by the application MoviePlayer. Other applications followed suit, and these two types have become accepted ways of saving a movie's location and looping state.
Opening URLs Let's finish up with something completely cool and incredibly easy to do. Namely, let's have our application tell the user's default Web b r o w s e r to open a Web page in a browser window. There are quite a few reasons you might want to do this. For one, your application might have a Help m e n u item {or perhaps a button in your About box) to send the user to your own website, so that you can provide up-to-date information about your product. Or, you might detect that the user's machine is lacking a d e c o m p r e s s o r needed for some data in the movies you want to play, so you w a n t the user to download the necessary files. For whatever reason, it's just plain useful to have at your disposal an easy w a y to open a Web browser and display a given URL. How easy can this be? Well, assuming that myMCis an open movie controller and that myHandle is a handle to a block of m e m o r y that contains a nullterminated string representing the URL, it's just this easy: MCDoAction(myMC, mcActionLinkToURL, (void *)myHandle) ;
That's right, with one line of code, w e ' v e instructed the movie controller to launch the user's default Web b r o w s e r {if it isn't already open) and display the specified URL in a new window. Of course, there is u n d o u b t e d l y some more code required to get the URL into a handle [supposing that it began its life as a typical C string of type char *). And, as w e ' v e seen, t h e r e ' s a fair a m o u n t of code r e q u i r e d to open a movie and associate a movie controller with it. But once we've done all that work, it's nice to know that we can leverage it to good effect. But you might be wondering: w h a t if we don't have a movie controller at hand? {Maybe we don't have any movie windows open yet.) W h a t do we do then? Well, we can simply open a movie controller. As I mentioned earlier, a movie controller is a software component that manages a user's interaction with a movie. Accordingly, we can use the Component Manager to open a movie controller, w h e t h e r or not we have a movie to attach it to. To do this, we just call the Component Manager's OpenADefaultComponent function and specify that we want an instance of a component of type MovieControllerComponentType. Listing 2.18 shows the complete function that takes a C string
48
Chapter2 Control
specifying a URL and does all the work necessary to open the specified URL in the user's default Web browser.
Listing 2.18 Opening a URL. OSErr QTApp_HaveBrowserOpenURL (char *theURL)
{
MovieController Handle Size OSErr
myMC = NULL; myHandl e = NULL; mySize = O; myErr = noErr;
/ / copy the specified URL into a handle mySize = (Size)strlen(theURL) + i ; i f (mySize == O) goto bai I ; / / allocate a new handle myHandle = NewHandleClear(mySize) ; i f (myHandle == NULL) goto bai I ; / / copy the URL into the handle BlockMove(theURL, *myHandle, mySize) ; / / instantiate a movie controller and send i t an mcActionLinkToURL message myErr = OpenADefaultComponent(MovieControl I erComponentType, O, &myMC); i f (myErr ! = noErr) goto bai I ; myErr = MCDoAction(myMC, mcActionLinkToURL, (void *)myHandle); bail 9 i f (myHandle i= NULL) Di sposeHandl e (myHandle) ; if
(myMC != NULL) C1oseComponent(myMC) ;
return (myErr) ;
Opening URLs
49
This is a generally useful technique to add to our bag of tricks: if there's a movie controller action that w e ' d like to issue but we don't have a movie controller close at hand, we can just call OpenADefau]tComponent to get a movie controller, issue the action, and then dispose of the controller. Movie controllers don't actually have to be controlling movies for us to put t h e m to work.
Conclusion After all this discussion of movie controllers and the things we can do with them, you might be wondering w h e t h e r it's possible to play back movies without the assistance of a movie controller. In other words, do we always have to have a movie controller lurking around somewhere? The answer is, yes and no. Some types of QuickTime movies require a movie controller while others do not. Standard linear QuickTime movies can be played back without the assistance of a movie controller, but of course you'll have to do a whole lot of work to replicate the user interaction normally provided by the movie controller. Other types of QuickTime movies, such as QuickTime VR and wired sprite movies, must have a movie controller associated with them. What's distinctive about the types of QuickTime movies that require movie controllers is that they are by nature highly interactive. You don't just sit back and watch a QuickTime VR movie; instead, you actively navigate within the node and within the multinode scene. The same is true for wired sprite movies: by and large, the real impact of a wired sprite movie arises from actions the user performs with the mouse. This isn't to deny that you could construct a QuickTime VR movie that navigated itself, sort of like a guided tour through a house or m u s e u m , which therefore was minimally interactive. But you couldn't manage the self-navigation without a movie controller. Still, some kinds of movies can be played back ~ la carte, as it were, without the assistance of a movie controller. In the next chapter, we'll take a look at how to use QuickTime's Movie Toolbox to do this.
50
Chapter2 Control
Out of Control
Introduction In the previous two chapters, we've focused almost exclusively on playing QuickTime movies using movie controllers. This is by far the easiest way to manage movies and to support the standard types of user interaction with movies. Indeed, some types of QuickTime movies cannot be presented to or managed by the user without a movie controller. But there are other types of QuickTime movies (most notably, standard linear movies) that can be played back without using a movie controller. In this chapter, we'll see how to accomplish this. In effect, our goal will be to provide all the capabilities that are provided by our basic application QTShell--for linear movies at least-but without the assistance of movie controllers. We'll show how to start and stop movies, provide basic editing services, and set various looping states. Why might we want to do this? Well, there are situations in which you want to play QuickTime movies but don't necessarily want the user to be able to manipulate those movies in the ways typically afforded by movie controllers. Perhaps the simplest example of this is found quite often in games, in which the application plays what's called a cut movie or a c u t s c e n e when the user completes one level and advances to the next. The cut movie serves as a transition from one level to the next, and you generally don't want the user interrupting or {heaven forbid!) editing your cut movie. So using the full-fledged movie controller interface is perhaps not the best way to handle QuickTime movie playback here. There are also more complicated examples of using controllerless QuickTime movies. For instance, we might want to use a linear QuickTime movie as a texture for a 3D object that's being displayed by our application. Figure 3.1 shows a window of the application TextureEyes, which uses QuickDraw 3D to define and render three-dimensional objects. In this window, we've created a cube and set its surface texture to be a QuickTime movie.
51
Figure 3.1
A QuickTime movie used as a texture on a 3D object.
Similarly, we might want to embed a QuickTime movie within another QuickTime movie. For instance, QuickTime VR allows us to draw into a panoramic node. We can use this capability to draw the individual frames of a QuickTime movie into a panorama, as shown in Figure 3.2. Here, the toy train on the table isn't part of the original panorama. Rather, we're drawing the frames of a standard QuickTime movie (one of whose frames is shown in Figure 3.3) into the panorama, using the QuickTime VR application programming interfaces and some QuickDraw routines to drop out the black background. The net effect is that the train appears to travel in a circle on the table, thereby imparting some motion and sound to the otherwise static and silent panorama. We might also want to embed a linear QuickTime movie into another linear QuickTime movie, as shown in Figure 3.4. The point is not that we couldn't have used movie controllers to help us display and manage the cut scenes or the textured or embedded QuickTime movies in these sorts of cases. Rather, the point is that we don't need to do so. In the examples we've mentioned, we just need a way to manage the display of the movie frames at the appropriate time (and to play back the movie's sound track, if it has one). So in those cases, the only services we really need are QuickTime's imaging, sound-producing, and timing services. We don't need the robust user interaction services provided by the standard linear movie controller.
52
Chapter 3 Out of Control
Figure 3.2
A QuickTime movie embedded into a QuickTime VR panorama.
Figure 3.3
The QuickTime movie embedded in Figure 3.2.
It's really a question of levels: the movie controller interface for controlling movies is a higher-level interface than what we need to accomplish the previously outlined tasks. Now we'll drop down a level and directly use the services of that part of QuickTime known as the Movie Toolbox. The Movie Toolbox is really the heart and soul of QuickTime. It provides services for opening movie files and extracting movies from them, playing movies, editing movies, creating movies, and much more. To a large degree, programming with QuickTime is using the Movie Toolbox.
Introduction
53
Figure 3.4 A QuickTime movie embedded into a QuickTime movie.
Figure 3.5
A movie window displayed by QTMooVToolbox.
So here's what we want to accomplish in this chapter: we want to modify our basic application QTShell so that it can open and play back linear QuickTime movies without using movie controllers. Let's call this new application QTMooVToolbox, in honor of the fact that we'll be doing everything using the Movie Toolbox. Since we're not using a movie controller, there will be no movie controller bar in our movie windows. A typical movie window will therefore look like the one in Figure 3.5. 54
Chapter 3 Out of Control
Figure 3.6 The Test menu for QTMooVToolbox.
As indicated, we want to support the standard movie editing capabilities, so our application's Edit m e n u will be unchanged {though, of course, the code providing the editing capabilities will change quite a bit}. We also want to provide the user with a way to start and stop the movie and to set normal and palindrome looping. Figure 3.6 shows the Test m e n u that we want to support. The last m e n u item here (Add Picture In Picture) is particularly interesting. It allows us to embed one linear QuickTime movie inside of another linear QuickTime movie {as shown in Figure 3.4}. As we'll see, it's fairly easy to use the Movie Toolbox to accomplish this.
Getting Started Setting Up to Use QuickTime Before moving forward with our QTMooVToolbox application, we need to back up a step or two to fill in a few details that we skipped over in the first two chapters. In order to use any of the Q u i c k T i m e services, we need to m a k e sure that the Q u i c k T i m e client s o f t w a r e is available on the target machine. On W i n d o w s computers, we can do this by trying to initialize the QuickTime Media Layer, like this: myErr = InitializeQTML(OL);
If I n i t i a l izeQTML completes successfully {that is, if myErr is noErr), then we k n o w that Q u i c k T i m e is available. On the o t h e r hand, if InitializeQTML doesn't complete successfully, then either QuickTime or some other essential c o m p o n e n t is missing, in w h i c h case we will just exit the application {perhaps after informing the user that QuickTime needs to be installed}. On Macintosh computers, we can call the Gestalt function to determine w h e t h e r QuickTime is available, as shown in Listing 3.1:
Getting Started
55
Listing 3.1 Determining whether QuickTime is available. Boolean QTUtils IsQuickTimelnstalled (void)
(
w
long OSErr
myAttrs; myErr = noErr;
myErr = Gestalt(gestaltQuickTime, &myAttrs) ; return(myErr == noErr) ;
On either Windows or Macintosh computers, once we've determined that QuickTime is indeed available in the current operating environment, we need to call the EnterMovies function, like so: myErr = EnterMovies(); EnterMovies initializes the Movie Toolbox, p e r f o r m i n g any necessary setup for our application (such as allocating any private storage it might need to use on our behalf). As with I n i t i a l izeQTML, we will check the value returned by EnterMovies and exit the application if an error occurs. When we are finished using QuickTime services--usually, w h e n our application is about to terminate--we should undo the work done by EnterMovies and (on Windows) InitializeQTML. We can call the ExitMovies function to terminate our connection with the Movie Toolbox. On Windows, we also need to call TerminateQTML to unregister our application from the QuickTime Media Layer.
Using Application-Specific Data Suppose now that our application is up and running, that we've successfully initialized QuickTime and the Movie Toolbox, and that the user has decided to open a movie file {perhaps using the Open c o m m a n d in the File menu). Our basic cross-platform framework calls the OTFrame_OpenMovielnWindow function to prompt the user for the movie file to open, create a w i n d o w object record to hold basic information about the movie and the file it is contained in, open a new w i n d o w on the screen, adjust the size of that w i n d o w to fit the movie's natural size, and do some other useful setup before the user can begin to interact with the movie. QTFrame_OpenMovielnWindow also calls the function OTApp_SetupWindowObject,
defined in the file ComApplication.c, to allow our application to perform any application-specific actions on the new movie before it is displayed to the user. Both of our previous applications (namely, QTShell and QTController) have
56
Chapter3 Out of Control
not put any code at all into QTApp_SetupWindowObject because the default con-
figuration was sufficient for our purposes then. Here, for QTMooVToolbox, we'll need to change that, for two reasons. First, we need to maintain some information for each open movie window beyond what is contained in the window object record. Second, since we are not using movie controllers in the QTMooVToolbox application, we need to do some additional work to prepare our movies for playback. Let's tackle the first task here and defer the second until a bit later in this chapter. We already have the apparatus in place to support adding applicationspecific data to our movie windows. As you may recall, the window object record includes the field fAppData, which is declared to be of type Handle. We put this field there precisely to provide ourselves a place to store any information that is specific to a particular application {since we don't want to be changing the framework files all the time). For QTMooVToolbox, we'll define a structure of type Appl icationDataRecord that contains three fields: typedef struct ApplicationDataRecord { MovieEditState fMovieEditState; / / the edit state of the movie Movie fPIPMovie; / / the picture-in-picture movie Rect fPIPRect; / / the picture-in-picture rectangle } Appl icationDataRecord, *Appl icationDataPtr, **Appl icationDataHdl ;
We don't need to explain these fields quite yet (although the comments should give you a reasonable idea of what they are for). But we do need to show how the application data record gets created and attached to the window object. We do this in the function QTApp_SetupWindow0bject defined in Listing 3.2. Listing 3.2 Setting up a window object. void QTApp_SetupWindowObject (WindowObject theWindowObject)
{
Movie ApplicationDataHdl OSErr
myMovie = NULL; myAppData = NULL; myErr = noErr;
i f (theWindowObject l: NULL) { / / get rid of the movie controller that's already been attached to this window object; / / we do this to illustrate how to manage movies without using a movie controller i f ((**theWindowObject).fController ! = NULL) { MCSetActionFi IterWithRefCon( (**theWindowObject). fControl ler, NULL, OL) ; Di sposeMovieControl I er( (**theWi ndowObject). fControl I er) ; (**theWindowObject) .fControl ler = NULL;
}
Getting Started
57
/ / allocate an application-specific data record myAppData = (ApplicationDataHdl )NewHandleClear(sizeof(Appl icationDataRecord)) ; (**theWi ndowObject), fAppData = (Handle)myAppData; i f (myAppData ! = NULL) { (**myAppData).fMovieEditState = NULL; (**myAppData).fPIPMovie = NULL; MacSetRect(&(**myAppData).fPIPRect, O, O, O, 0);
}
/ / some lines omitted here
Notice that the first thing we do in QTApp_SetupWindowObject is get rid of the movie controller that has already been created by the time QTApp_SetupWindowObject is called. That's because (as you already know) we want to illustrate how to manage movies without using a movie controller. We might as well just dispose of the controller, so we're sure it's not doing any w o r k for us. The second thing we do in OTApp_SetupWindowObject is call NewHandleClear to allocate an application data record and initialize its fields. (Of course, the initialization here is overkill, since NewHandleClear zeros the bytes in the newly allocated block; nevertheless, it's useful to make this initialization explicit.) Then we set the fAppOata field of the w i n d o w object passed to QTApp_SetupWindowObject to the newly allocated handle. This allows us, as we intended, to keep a copy of this record for each movie window. The file ComFramework.c defines a couple of functions that facilitate retrieving the application-specific data attached to a movie. For instance, we can use the function OTFrame_GetAppOataFromWindowObject, defined in Listing 3.3, to retrieve the application-specific data associated with a w i n d o w object.
Listing 3.3 Getting the application-specific data attached to a window object. Handle QTFrame_GetAppDataFromWindowObject (Wi ndowObject theWindowObject)
{
/ / make sure this is a window object belonging to our application i f ( !QTFrame_IsWindowObjectOurs (theWi ndowObject) ) return(NULL) ; return ( (**t heWindowObject). fAppData) ;
Similarly, we can use the function QTFrame_GetAppDataFromWindow, defined in Listing 3.4, to retrieve the application-specific data attached to a particular movie window.
58
Chapter 3 Out of Control
Listing 3.4 Getting the application-specific data attached to a movie window. Handl e QTFrame_GetAppDataFromWindow (Wi ndowReference theWi ndow)
{
WindowObject
myWindowObject = NULL;
myWindowObject = QTFrame_GetWindowObjectFromWi ndow(theWi ndow) ; i f (myWindowObject == NULL) return(NULL); return (QTFrame_GetAppDataFromWi ndowObject (myWindowObject) ) ;
We'll use these functions extensively throughout the rest of this chapter.
Updating the Application Framework The basic framework for playing QuickTime movies that we've used for the QTShell and QTController sample applications makes extensive use of movie controller components to manage the movie playback and any user interaction with the movie. It uses movie controllers to help set the window size, handle movie editing, process movie-related events, and do a handful of other important tasks. So, I must admit that I was more than a little worried when I set about to develop the QTMooVToolbox application for this chapter. After all, the very first application-specific thing we do (in Listing 3.2) is throw away the movie controller that the framework so kindly created and configured for us. How much of the entire framework was dependent upon work done by that movie controller? Or, asked another way, how much of the basic QTShell framework would we need to rewrite in order to play back movies using only the Movie Toolbox? Surprisingly, very little had to change. In fact, only three functions needed reworking to handle the new assumption that the movie window might no longer have a movie controller associated with it. First of all, the OTFrame_Si zeWindowToMovie function originally contained these lines: myMC = (**theWindowObject).fController; myMovie = (**theWindowObject).fMovie; i f (MCGetVisible(myMC)) MCGetControl I erBoundsRect (myMC, &myMovieBounds) ; else GetMovi eBox(myMovie, &myMovieBounds) ;
This just says" if the movie has a visible movie controller bar, then get the desired window size by calling MCfietControl 1erBoundsRect (which returns the
Getting Started
59
rectangle that encloses the movie and the movie controller bar); otherwise, call GetMovieBox (which returns the rectangle that encloses just the movie). To allow for the case w h e r e t h e r e is no m o v i e c o n t r o l l e r at all, w e can rewrite the key lines like this: GetMovieBox(myMovie, &myMovieBounds) ; i f (myMC i= NULL) i f (MCGetVisible(myMC)) MCGetControl I erBoundsRect(myMC, &myMovieBounds) ;
Second, the framework function QTFrame_HandleEditMenultem also assumed that every movie window had a movie controller associated with it, in which case it could use movie controller functions like MCCut and MCCopy to handle the Edit m e n u items. In QTMooVToolbox, this is no longer true, so we need to provide a way for an application to intercept the Edit m e n u c o m m a n d s and do its own type of movie editing. We can do this very simply by inserting a call to OTApp_HandleMenu before OTFrame_HandleEditMenultem handles the menu items. If QTApp_HandleMenu returns the value true to indicate that it has handled the m e n u item, then we just skip the rest of QTFrame_Handl eEdi tMenuItem. We'll see later in this chapter how our application actually handles the Edit menu items; for the moment, at least we have successfully provided a way to prevent the framework from trying to handle Edit m e n u items using movie controller functions. The last important change we need to make to our basic framework is to provide a way for both our Macintosh and Windows applications to tell the Movie Toolbox to play any movies that are open. When we're using QuickTime, we don't just start a movie playing and then forget about it, expecting perhaps that it will play through until the end and then stop. Instead, from time to time, we need to explicitly allocate some processor time to the Movie Toolbox so that it can do whatever it needs to do in order to keep the movie playing. This periodic allocation of processor time to the Movie Toolbox is k n o w n as tasking a movie, and we can accomplish it by calling the MoviesTask function over and over until the movie is done playing. So w h a t we need to do now is provide a way for QTMooVToolbox to call MoviesTask periodically. (When we are using movie controllers, we never need to call MoviesTask because the MCIsPlayerEvent function calls it internally for us.) A standard way to do this on the Macintosh is to insert a call to MoviesTask in the application's main event loop. For several reasons (which we don't need to go into), we'll adopt a slightly different technique in QTMooVToolbox; we'll call MoviesTask in response to null events (which occur often enough to keep the movie playing smoothly). For Windows applications, however, this is slightly trickier, since there is no Windows equivalent of the Macintosh null or idle event (and there is no main event loop). Probably the
60
Chapter3 Out of Control
best solution for Windows applications would be to install a timer that calls MoviesTask periodically. For the moment, however, we'll adopt a different (and much simpler) approach. Our basic Windows framework already calls the function OTApp_HandleEvent every time a movie window receives a message to process. So we'll just look for null events generated by QTML there and call QTApp_Iclle (which in turn calls MoviesTask) for every open movie window. Listing 3.5 shows the changes we can make to QTApp_HandleEvent to keep our Windows movies running smoothly.
Listing 3.5 Getting Windows applications to call QTApp_ldle. Boolean QTApp_HandleEvent (EventRecord *theEvent)
(
Boolean
mylsHandled = fal se;
# i f TARGET OS WIN32 WindowReference m
myWindow = NULL;
i f (theEvent I= NULL) i f (theEvent->what == nullEvent) { myWindow = QTFrame_GetFrontMovieWi ndow() ; while (myWindow ! = NULL) { QTApp_Idl e (myWindow) ; myWindow = QTFrame_GetNextMovieWi ndow(myWindow) ;
}
}
#endif return (mylsHandl ed) ;
Keep in mind that these strategies might not work for more complicated or more specialized applications. On the Macintosh, for instance, an application might be doing so much processing that there are not enough null events generated to keep the movie playing smoothly. Conversely, our Windows strategy might consume too much processor time with its almost constant calls to QTApp_Idle. So QTMooVToolbox most certainly does not provide a universal solution to the problem of when and how to call MoviesTask. The strategies that we've adopted here are designed only to keep the movies running smoothly, with a minimum of changes to our basic QuickTime framework.
Getting Started
61
Preparing Movies for Playback So far, our application can open QuickTime movie files, read the movies from those files, and create appropriately sized windows to hold those movies. Are we finally ready to begin playing the movies? In other words, w h e n the user indicates that he or she wants the movie to begin playing, can we just start the ball rolling by calling the StartMovie function and then let our framework task the movie periodically? The answer here is, yes and no. We could, indeed, just call StartMovie, and most movies would start playing pretty much immediately, as desired. Other movies might require a small but noticeable amount of time to perform some necessary setup activities, such as allocating buffers to hold data from the movie file and setting up various QuickTime components to handle the process of decompressing the data in the file. This setup is unavoidable; however, if our application doesn't start the movie playing immediately, we can perform this setup right after the user opens the movie and before the user actually starts playing the movie so that it is effectively unnoticeable. This process is called prerolling the movie. For some movies--in particular, for streamed movies located remotely on the Web or on file servers--prerolling alone is not sufficient to guarantee an optimal user experience. That's because some actions need to occur even before the movie can successfully be prerolled. QuickTime needs to set up connections to the remote servers and negotiate one or more protocols for exchanging data; it also needs to get information about the types of data in those remote movies. This process is called preprerolling the movie {because it needs to occur prior to the prerolling process}. Unless we prepreroll movies stored remotely, the movie playback would be annoyingly choppy, and some of the data in those movies (for instance, the sound} might not play correctly at all. So we need a more general method of starting up movies that will accommodate both local and remote QuickTime movies. We need to prepreroll and preroll our movies. Let's consider these operations in reverse order.
Prerolling Movies To preroll a movie, we can use the Movie Toolbox function Prerol IMovie. As indicated, PrerollMovie does whatever it can to prepare the movie for immediate playback once the movie is started. This involves determining w h a t kinds of data are in the movie and setting up the appropriate software components to handle those kinds of data. Prerolling might also involve actually reading some of the movie's data and getting the relevant decompressors ready to decompress that data. Because of this, PrerollMovie needs to know the current location in the movie and the rate at which the movie is to be played. We can get the current location in the movie by calling GetMovieTime,
G2
Chapter3 Out of Control
and we can get the m o v i e ' s default p l a y b a c k rate by calling GetMoviePreferredRate. So we can preroll a movie by executing these lines of code: myTimeValue = GetMovieTime(myMovie, NULL); myPl ayRate = GetMoviePreferredRate (myMovie) ; PrerolIMovie(myMovie, myTimeValue, myPlayRate) ;
If for some reason you want to change the current location in the movie or the movie's default playback rate, you should do it before calling Prerol 1Movie so that you don't invalidate the work done by PrerollMovie. You can set the current location in the movie by calling SetMovieTime, and you can set the default playback rate by calling SetMovi ePreferredRate. The default playback rate is the rate that StartMovie uses w h e n it starts a movie playing. When you open a movie from a movie file, the actual playback rate is 0 (that is, the movie is not playing).
Preprerolling Movies To prepreroll a movie, we can use the Movie Toolbox function PrePrerol l Movie, which establishes any required network connections between the computer running our application and the computer that contains the movie being preprerolled. If the movie resides on the same computer as the application (that is, if the movie is a local movie), then PrePrerol 1Movie has nothing to do and it returns immediately. As a result, there is no harm in calling PrePrerol IMovie for every movie our application opens. On the other hand, if the movie being preprerolled is not a local movie, then preprerolling it can take a significant amount of time, particularly if the local computer needs to access the remote computer over a dialup line. To keep your application from stalling while QuickTime negotiates a connection with the remote computer, you can call PrePrerollMovie asynchronously, in which case PrePrerollMovie returns almost immediately and then executes an application-defined completion function w h e n the preprerolling is finished. PrePrerollMovie has the same calling interface as PrerollMovie, except that you must also specify a movie preprerolling completion routine and an optional reference constant that is passed to the completion routine" myPl ayRate = GetMoviePreferredRate (myMovie) ; PrePrerolIMovie(myMovie, O, myPlayRate, gMoviePPRollCompleteProc, (void *)theWindowObject) ;
Here, gMoviePPRollCompleteProc is a universal procedure pointer (UPP)to the movie preprerolling completion routine. QTMooVToolbox allocates this UPP w h e n it starts up, in the function QTMT_Init defined in Listing 3.6.
Preparing Movies for Playback
G3
Listing 3.6 Allocating a routine descriptor for QTMT_MoviePrePrerollCompleteProc. void QTMT_Init (void)
(
}
/ / allocate a routine descriptor for our prepreroll completion routine gMovi ePPRol I CompleteProc = NewMoviePrePrerol l CompleteProc ( QTMT_MoviePrePrerol I CompleteProc) ;
When the preprerolling is complete, the Movie Toolbox executes our movie preprerolling completion function, defined in Listing 3.7.
Listing 3.7 Respondingwhen a movie is done preprerolling. PASCAL_RTN void QTMT_MoviePrePrerol l CompleteProc (Movie theMovie, OSErr thePrerolIErr, void *theRefCon)
{
#pragma unused(thePrerol l Err) WindowObject Appl icationDataHdl TimeValue Fixed
myWindowObject = (Wi ndowObject) theRefCon; myAppData = NULL; myTimeVal ue; myPl ayRate;
myAppData = (Appl i cat i onDataHdl ) QTFrame_GetAppDataFromWi ndowObject (myWindowObject) ; i f ((theMovie == NULL)II (myWindowObject == NULL)II (myAppData == NULL)) return; / / preroll the movie, so that i t ' s ready to play when we call StartMovie myTimeValue = GetMovieTime(theMovie, NULL); myPl ayRate = GetMoviePreferredRate(theMovie) ; PrerolIMovie(theMovie, myTimeValue, myPlayRate) ; / / for the picture-in-picture movie, start i t playing in a loop i f (theMovie == (**myAppData).fPIPMovie) { QTMT_SetMovieLoopState(theMovie, kNormalLooping) ; StartMovi e(theMovie) ;
}
As you can see, our movie preprerolling completion function is quite simple. It really just prerolls the movie, thereby making the movie ready for playback. For some movies (namely, for QTMooVToolbox's "picture-in-
64
Chapter 3 Out of Control
picture movies"), it also starts the movies playing in a loop. We'll discuss picture-in-picture movies later. Note that preprerolling and prerolling are required only for movies that we are managing directly, using the Movie Toolbox. If we use the movie controller interface for managing movies, the movie controller automatically preprerolls and prerolls the movies for us.
Playing Movies Now we are finally ready to start our movie playing. Assuming that a movie has already been preprerolled and prerolled, we can set it in motion by setting its rate to a nonzero value, using the SetMovieRate function. Or, we can simply call the StartMovie function, which sets the movie rate to the default {or "preferred") movie rate. QTMooVToolbox defines the function QTMT_ StartMovie (shown in Listing 3.8) to start a movie. Listing 3.8 Starting a movie playing. void QTMT_StartMovie (Movie theMovie)
{
i f (theMovie == NULL) return; / / i f we aren't looping and we're at the end of the movie, / / return to the beginning of the movie i f (!QTMT_IsMovieLooping(theMovie)) i f (GetMovieTime(theMovie, NULL) == GetMovieDuration(theMovie)) GoToBeginni ngOfMovie(theMovi e) ; StartMovi e(theMovi e) ;
QTMT_StartMovie checks to see whether a nonlooping movie has reached its end; if it has, QTMT_StartMovie calls the Movie Toolbox function GoToBeginningOfMovie to reset the current movie time to the beginning of the movie. If we had been playing the movie using a movie controller, the movie controller would have done this for us. Since we're programming at a slightly lower level, we need to do some of this housekeeping ourselves. Similarly, w h e n we are playing a nonlooping movie using the Movie Toolbox and the movie reaches its end, its rate is not automatically set to O. Again, this is some minor housekeeping that we need to take care of ourselves. In this case, we'll do the appropriate check in our QTApp_Idl e routine, defined in Listing 3.9.
Playing Movies
65
Listing 3.9 Performing idle-time tasks. void QTApp_Idle (WindowReference theWindow)
{
GrafPtr WindowObject ApplicationDataHdl
mySavedPort; myWindowObject = NULL; myAppData = NULL;
GetPort (&mySavedPort) ; MacSetPort (QTFrame_GetPort FromWindowReference(theWi ndow)) ; myWindowObject = QTFrame GetWindowObjectFromWindow(theWi ndow) ; myAppData = (AppI i cat i onDataHdl ) QTFrame_GetAppDataFromWi ndow(theWi ndow) ; i f ((myWindowObject I= NULL) && (myAppData ! = NULL)) { i f (!QTMT_IsMovieLooping ( (**myWi ndowObject), fMovie)) i f (GetMovieTime((**myWindowObject).fMovie, NULL) == GetMovi eDurat i on ( (**myWi ndowObject). fMovi e) ) i f (GetMovieRate((**myWindowObject). fMovie) > O) SetMovieRate((**myWindowObject). fMovie, O) ; i f ((**myWindowObject).fMovie ! = NULL) MoviesTask((**myWindowObject). fMovie, DoTheRightThing) ; i f ((**myAppData).fPIPMovie I= NULL) QTMT_DrawPicture I nPi ctureMovi e (myWindowObject) ; MacSetPort (mySavedPort) ;
QTApp_Idle does three main things. It sets the movie rate of the movie in the specified window to 0 if it isn't looping and it has played to its end; it tasks the movie; and it draws any picture-in-picture movie in the specified window. (Once again, we'll discuss picture-in-picture movies at more length later.) As you can see, MoviesTask takes two parameters; these are the movie to be tasked and a maximum number of milliseconds that MoviesTask should spend doing its work. Movies.h defines the constant DoTheRightThing as 0, which means to service the specified movie once and then return. So, at last, we have at hand all the essential elements of playing movies using the Movie Toolbox. First, we open a movie file and load the movie from it (using the same code that we used w h e n playing movies using movie controllers). Then, we prepreroll and preroll that movie. Then, we start the movie playing by calling StartMovie. And finally, we periodically call MoviesTask to give the movie some time to play. It's OK to call MoviesTask on a
66
Chapter 3 Out of Control
movie that has finished playing, so in QTApp_Idle we don't bother to check w h e t h e r the movie is actually playing or not. In fact, we shouldn't make that check, since we need to call MoviesTask on a movie while it is preprerolling as well as while it is playing.
Editing Movies Editing movies is one of the tasks at w h i c h movie controllers excel. A movie c o n t r o l l e r k e e p s t r a c k of a m o v i e ' s c u r r e n t s e l e c t i o n a n d allows us (for instance) to cut that selection from that movie by calling the function MCCut, like this" case IDM EDITCUT: myEdi tMovi e = MCCut(myMC) ; break; m
As you can see, MCCut also returns to us a movie that is the cut selection, which we can put on the scrap {to make it available for subsequent pasting) by calling PutMovie0nScrap. Moreover, the movie controller keeps track of the current edit state of the movie that exists before each editing operation, so that it can restore that state if we subsequently call MCUndo. A movie's current edit state consists of all the information that describes the movie's contents, such as the sound and video data in the movie; the current edit state also includes the movie's current selection. The Movie Toolbox provides a set of high-level movie editing functions, which are just as easy to use as the functions provided by movie controllers. For instance, to cut the current movie selection, we can call the CutMovieSelect i on function. case IDM EDITCUT" myEditMovie = CutMovieSelection(myMovie) ; break;
Similarly, we can use the ClearMovieSelection function to clear the c u r r e n t movie selection (that is, r e m o v e it f r o m the movie w i t h o u t r e t a i n i n g the removed data). case IDM EDITCLEAR. Cl earMovi eSel ecti on (myMovi e) ; break; m
Moreover, the Movie Toolbox provides the SetMovieSelection function, which we can use to handle the Select All m e n u item in the Edit menu, like this"
Editing Movies
67
case IDM EDITSELECTALL" SetMovieSelection(myMovie, OL, GetMovieDuration(myMovie)) ; break;
There is one complication here: if we want to provide the standard movie editing behavior without using a movie controller, then we also need to keep track of the movie's current edit state w h e n e v e r we perform any editing operations. As w e ' v e seen, the application data record for QTMooVToolbox contains a field fMovieEditState, of type MovieEditState, which we'll use to hold the current edit state. Before we call any Movie Toolbox function that changes the contents of a movie, we can call the NewMovieEditState function to create a new movie edit state, as in this snippet from QTApp_HandleMenu" i f ((theMenultem-= IDM_EDITCUT) [[ (theMenultem == IDM_EDITPASTE) [[ (theMenultem == IDM EDITCLEAR)) ( i f ((**myAppData).fMovieEditState l= NULL) Di sposeMovieEdi tState ( ( **myAppData). fMovi eEdi tState) ; (**myAppData). fMovieEdi tState = NewMovieEditState(myMovie) ;
Notice that we need to dispose of any existing edit state (by calling Oi sposeMovi eEdi tState) before creating a new one. Otherwise, w e ' d be leaking memory. If the user selects the Undo m e n u item in the Edit menu, we need to reinstate the movie edit state that we have stored in our application data record. We do this by calling UseMovieEdi tState, like this: case IDM EDITUNDO: i f ((**myAppData).fMovieEditState == NULL) break; UseMovieEditState(myMovie, (**myAppData).fMovieEditState) ; Di sposeMovieEdi tState ( ( **myAppData). fMovi eEdi tState) ; (**myAppData). fMovieEdi tState = NULL; break;
Once we have used an edit state, we want to dispose of it and set the stored edit state reference to NULL so that we don't reinstate the same edit state. QTMooVToolbox supports only one level of undo, just like the standard linear movie controller. In theory, however, you can use NewMovieEditState and UseMovieEditState to support multiple levels of undo. Implementing this feature is left as an exercise for the reader.
68
Chapter3 Out of Control
There is one final complication to consider: if the user selects the entire movie and then cuts the current selection, the resulting movie is empty. It still exists, but it has no video or audio data in it. Since the movie has no video data, the movie box is empty. Now, if we had performed this cut using movie controller functions, the movie controller would have detected that the size of the movie had changed and would have notified our application by sending it the mcActionControllerSizeChanged movie controller action; in
response to this action we would have called our application's QTFrame_Si zeWindowToMovie function. But we didn't use movie controller functions t o p e r form the cut, so we need to handle this possibility ourselves. The simplest way to handle this situation is just to call QTFrame SizeWindowToMovie whenever we perform any editing operation that might have changed the size of the movie, like this" i f ((theMenultem == IDM_EDITUNDO) II (theMenultem == IDM_EDITCUT) II (theMenultem == IDM_EDITPASTE) II (theMenultem == IDM_EDITCLEAR)) { QTFrame_Si zeWindowToMovie (myWindowObject) ; (**myWi ndowObject), flsDi rty = true;
}
We now have all the pieces that we need to handle the Edit menu commands without using movie controllers. Listing 3.10 shows the QTApp_ HandleMenu function for QTMooVToolbox. (For readability, I've cut out the Test menu handling.)
Listing 3.10 Handling Edit menu items using the Movie Toolbox. Boolean QTApp_HandleMenu (UInt16 theMenultem) WindowObject AppI i cat i onDataHdl Movie Movie Boolean myWindowObject
=
myWindowObject = NULL; myAppData = NULL; myMovie = NULL; myEditMovie -- NULL; myI sHandl ed = false;
QTFrame GetWindowObjectFromFrontWi ndow()
;
i f (myWindowObject l= NULL) { myAppData = (Appl i cat i onDataHdl ) QTFrame_GetAppDataFromWi ndowObject (myWindowObject) ; myMovie = (**myWindowObject), fMovi e;
}
i f ((myWindowObject : : NULL)II (myAppData : : NULL)II (myMovie : : NULL)) return (mylsHandl ed) ;
Editing Movies
69
/ / before the Cut, Paste, and Clear commands, get the current edit state, / / after disposing of the currently saved edit state i f ((theMenultem == IDM_EDITCUT) JJ (theMenultem == IDM_EDITPASTE) JJ (theMenultem == IDM_EDITCLEAR)) { i f ((**myAppData).fMovieEditState != NULL) Di sposeMovi eEdi tState ((**myAppData). fMovi eEdi tState) ; (**myAppData). fMovieEdi tState = NewMovieEditState(myMovie) ;
}
switch (theMenultem) { / / Edit menu items; / / here we are overriding the framework's Edit menu handling / / (which uses movie controllers) case IDM EDITUNDO: / / restore the movie using the currently saved edit state, then dispose of i t i f ((**myAppData).fMovieEditState == NULL) break; UseMovieEditState(myMovie, (**myAppData).fMovieEditState) ; Di sposeMovi eEdi tState ((**myAppData). fMovi eEdi tState) ; (**myAppData) .fMovieEditState = NULL; break; case IDM EDITCUT. myEdi tMovi e = CutMovieSel ect i on (myMovie) ; break; D
case IDM EDITCOPY: myEditMovie = CopyMovieSelection(myMovie) ; break; case IDM EDITPASTE: myEdi tMovi e = NewMovieFromScrap(newMovieAct i ve) ; i f (myEditMovie ! = NULL) { PasteMovieSelection(myMovie, myEditMovie) ; Di sposeMovi e (myEditMovi e) ; myEdi tMovie = NULL;
}
break; case IDM EDITCLEAR: Cl earMovi eSel ecti on (myMovie) ; break;
70
Chapter3 Out of Control
case IDM EDITSELECTALL: SetMovieSelection(myMovie, OL, GetMovieDuration(myMovie)) ; break; / / Test menu items omitted here default: break; } / / switch (theMenultem) / / place any cut or copied movie segment onto the scrap i f (myEditMovie l= NULL) { PutMovieOnScrap(myEditMovie, OL) ; Di sposeMovie (myEditMovi e) ;
}
/ / after any commands that might have changed the movie, sync up the movie window size / / and mark the window as dirty i f ((theMenuItem == IDM_EDITUNDO) II (theMenultem == IDM_EDITCUT) II (theMenultem == IDM_EDITPASTE) I I (theMenultem == IDM_EDITCLEAR)) { QTFrame_Si zeWindowToMovie (myWindowObject) ; (**myWindowObject).flsDirty = true;
}
/ / for any Edit menu command, indicate that we handled i t i f (MENU_ID(theMenultem) == kEditMenuResID) myIsHandled = true; return (mylsHandl ed) ;
All in all, handling the Edit m e n u items using Movie Toolbox functions is not too much more complicated than doing it using the movie controller functions. We need to manage the movie edit states ourselves, and we need to make sure to resize a changed movie. But otherwise, the code is quite reminiscent of the movie controller editing code.
Looping Movies In the previous chapter, you might recall, we saw h o w to read a m o v i e ' s looping state from some user data stored in the movie file. If a movie file contains a user data item of type 'LOOP', then the data in that item determines w h e t h e r the looping state is normal looping (data is O) or palindrome looping [data is 1). If there is no such user data item in the movie file, then
Looping Movies
71
we assume that the movie does not loop. To read a movie's looping state, we defined the function QTUti ] s GetMovieFi ]eLoopingInfo, w h i c h returns one of three constants defined in our header file QTUti ] i t i es. h: m
enum{ kNormal Looping kPal i ndromeLoopi ng kNoLooping
!;
= 0, = 1,
=2
To set a movie's looping state based on the looping state stored in the file, we called the MCDoAction function, passing the two movie controller actions mcActionSetLooping and mcActionSetLoopIsPalindrome with appropriate combinations of true and fa]se. Now we want to see how to accomplish the same thing without using these movie controller actions. To manipulate a movie's looping state without using movie controller functions, we need to w o r k with the movie's time base, w h i c h defines the time coordinate system of the movie. We can think of a movie's time base like a vector that defines the direction and velocity of time for a movie. A movie's time base also defines the current movie time. So it makes sense that time bases are associated with looping: normal looping involves simply resetting the current movie time to 0 once the movie time reaches the end, whereas palindrome looping involves reversing the direction of time while keeping the velocity the same. The Movie Toolbox creates a time base for us w h e n we load a movie. Thereafter, we can retrieve a movie's time base by calling the GetMovieTimeBase function. The movie's looping state is determined by some of the time base control flags, a 32-bit value, which we can get and set by calling the functions GetTimeBaseFlags and SetTimeBaseFlags. The header file Movies.h defines these two time base flags related to looping: enum{ loopTimeBase pa 1 i ndromeLoopTi meBase
};
=1, =2
These constants are interpreted as bit masks for the time base control flags. In other words, you should think of ]oopTimeBase as 1L << 0 and pa] indromeLoopTimeBase as 1L << 1. As you might have guessed, we can set a movie's looping state by getting the current time base control flags, setting the appropriate bits on or off, and t h e n setting the revised time base control flags. Here's how we would enable palindrome looping:
72
Chapter 3 Out of Control
TimeBase long
myTimeBase = GetMovieTimeBase(theMovie); myFlags = GetTimeBaseFlags (myTimeBase) ;
myFlags [= loopTimeBase; myFlags [= pal indromeLoopTimeBase; SetTimeBaseFlags(myTimeBase, myFlags) ;
For optimal playback performance, however, we should take one further step, which is to set the movie's playback hints to reflect the desired looping state. A movie's playback hints specify one or more optimizations that the Movie Toolbox should use during movie playback. Once again, the playback hints are defined using individual bits in a 32-bit long word. These two bits are defined for setting movie looping hints: enum { hintsLoop hintsPal i ndrome
};
=1<<1, =1<<9
The Movie Toolbox provides the SetMoviePlayHints function for setting a movie's playback hints, but there is no corresponding fietMovieVlayHints function to get the current playback hints. Instead, SetMovieVlayHints requires us to pass it a 32-bit mask that determines which of the bits in the 32-bit play hints long word are to be considered. For example, to set the palindrome looping play hint on, we could do this: SetMoviePlayHints(theMovie, hintsLoop, hintsLoop); SetMoviePlayHints(theMovie, hintsPal indrome, hintsPal indrome) ;
These two lines of code set both the looping and palindrome looping play hints to be active. (Remember that looping ~must be enabled in order for palindrome looping to be enabled.) And to set the movie play hints to disable any looping, we could execute this line: SetMoviePlayHints(theMovie, OL, hintsLoop [ hintsPalindrome);
So, putting this all together, we see that to set the looping state of a movie, we need to set the time base control flags appropriately; we also should set the movie's playback hints to optimize playback performance. Listing 3.11 defines our complete QTMT_SetMovieLoopState for setting a movie's looping state.
Looping Movies
731
Listing 3.11 Setting a movie's looping state. void QTMT_SetMovieLoopState (Movie theMovie, UInt8 theLoopState)
{
Ti meBase long
myTi meBase; myFlags = OL;
i f (theMovie == NULL) return; myTimeBase = GetMovieTimeBase(theMovie) ; myFl ags = GetTimeBaseFlags (myTimeBase) ; switch (theLoopState) { case kNormalLooping: SetMoviePlayHints(theMovie, hintsLoop, hintsLoop); SetMoviePlayHints(theMovie, OL, hintsPalindrome) ; myFlags I = loopTimeBase; myFlags &= ~pal indromeLoopTimeBase; break; case kPal i ndromeLoopi ng: SetMoviePlayHints(theMovie, hintsLoop, hintsLoop); SetMoviePlayHints(theMovie, hintsPal indrome, hintsPal indrome) ; myFlags I = loopTimeBase; myFlags I = palindromeLoopTimeBase; break; case kNoLooping: default: myFlags &= ~loopTimeBase; myFlags &= ~pal indromeLoopTimeBase; SetMoviePlayHints(theMovie, OL, hintsLoop I hintsPalindrome); break; SetTimeBaseFlags(myTimeBase, myFlags) ;
For certain purposes, we sometimes want to know w h e t h e r a movie is looping and w h e t h e r it is palindrome looping. Based on what we've learned so far, we can easily define the function OTMT_IsMovieLooping shown in Listing 3.12, which returns a Boolean value that indicates w h e t h e r looping {either normal or palindrome} is enabled for the specified movie.
14
Chapter 3 Out of Control
Listing 3.12 Determining whether a movie is looping. Boolean QTMT_IsMovieLooping (Movie theMovie)
{
Ti meBase long
myTi meBase; myFl ags = OL;
i f (theMovie == NULL) return(false); myTimeBase = GetMovieTimeBase(theMovie) ; myFl ags = GetTimeBaseFlags (myTimeBase) ; return((Boolean) (myFlags & loopTimeBase)) ;
And we can easily define the function QTMT_IsMoviePalindromeLooping, shown in Listing 3.13, to tell us whether palindrome looping is enabled for the specified movie.
Listing 3.13 Determining whether a movie is palindrome looping. Boolean QTMT_IsMoviePalindromeLooping (Movie theMovie)
{
Ti meBase l ong
myTi meBase; myFl ags = OL;
i f (theMovie == NULL) return(false); myTimeBase = GetMovieTimeBase(theMovie) ; myFl ags = GetTimeBaseFlags (myTimeBase) ; return((Boolean) ((myFlags & loopTimeBase) && (myFlags & palindromeLoopTimeBase)));
Our application QTMooVToolbox uses these functions to set the appropriate check marks on the Loop and the Loop Back And Forth menu items in the Test menu, like this: QTFrame_SetMenultemCheck(myMenu, IDM_SET_LOOPING, QTMT_IsMovieLooping(myMovie) && !QTMT_IsMoviePalindromeLooping(myMovie)) ; QTFrame_SetMenuItemCheck(myMenu, IDM_SET_PALINDROME_LOOPING, QTMT_IsMovi ePaI i ndromeLoopi ng (myMovi e) ) ;
Looping Movies
75
Playing Picture-in-PJdure Movies Last Christmas, I was hoping that Santa Claus would bring me a new television set, perhaps a bit larger than the one I have now, and possibly also w i t h the so-called picture-in-picture capability, where you can tune into a second channel that's displayed in a smaller box in the corner of the main picture. Well, Santa wasn't that generous this year, so I decided to see w h e t h e r QuickTime could help satisfy my craving to watch two video sources at once (see Figure 3.4 once again). It turns out that it's surprisingly straightforward to use the Movie Toolbox to embed one movie inside of another movie. Let's call the movie that we are embedding the picture-in-picture movie and the movie we're embedding it into the main movie. In the broadest outlines, we want to open the movie file that contains the picture-in-picture movie, set that movie's graphics port to be the w i n d o w that already contains the main movie, and then set the picture-in-picture movie's enclosing rectangle to fit within the main movie window. Then we need to ensure that the main movie doesn't draw on top of the picture-inpicture movie; we do this by setting the main movie's display clipping region to exclude the rectangle occupied by the picture-in-picture movie. Finally, we need to task both movies in order to update the movie images and play the movies' sound. Let's see how to do these things.
Setting the Picture-in-Picture Movie Geometry Since we want to draw the picture-in-picture movie in the same w i n d o w as the main m o v i e - - a n d indeed on top of the main m o v i e - - w e need to set the graphics port of the picture-in-picture movie to the same graphics port used by the main movie. We can do that quite easily, like this: GetMovieGWorld(myMainMovie, &myGWorld, NULL); SetMovieGWorld(myPIPMovie, myGWorld, NULL) ;
But we don't want to draw the picture-in-picture movie at its natural size. Instead, we want it to occupy some smaller portion of the main movie. For simplicity, we'll set the picture-in-picture movie to have a height and w i d t h that are one-third those of the main movie. GetMovieBox(myMainMovie, &myRect) ; myValue : (myRect.bottom - myRect.top) / 3; myRect.top +: myValue; myRect.bottom -= myValue;
76
Chapter3 Out of Control
myValue = (myRect.right - myRect.left) / 3; myRect.left += myValue; myRect.right -= myValue; SetMovieBox(myPIPMovie, &myRect) ;
Note that this strategy will result in the picture-in-picture movie being distorted, unless its original dimensions are proportional to the dimensions of the main movie. It would be easy, however, to modify this code to retain the original proportions of the picture-in-picture movie while limiting its largest dimension to one-third the height or width of the main movie. [This refinement is left as an exercise for the reader.I Finally, let's keep track of the picture-in-picture movie and its new rectangle in the application data structure attached to the main movie window. (**myAppData). fPIPRect = myRect; (**myAppData).fPIPMovie = myPIPMovie;
Setting the Main Movie's Display Clipping Region If we simply started playing the picture-in-picture movie and the main movie using the geometry we've set up so far, we would get an annoying flickering caused by the main movie image being drawn over the existing picture-in-picture movie image, which in turn is then refreshed over the main movie image. To avoid this, we want to set up the main movie so that it doesn't draw within the rectangle that encloses the picture-in-picture movie. We can do this by defining a new display clipping region for the main movie. A movie's display clipping region is the portion of the movie box in which the movie image is to be drawn. That is to say, the display clipping region defines a clipping region that is applied to a movie just before it is displayed. [A movie also has a clipping region, but that region is applied much earlier in the imaging pipeline and isn't relevant to us here.I By default, a movie's display clipping region exactly coincides with its movie box, but it's possible to set any subregion of the movie box as the display clipping region. In the current situation, we want the main movie's display clipping region to be the rectangle occupied by the main movie minus the rectangle occupied by the picture-in-picture movie. To calculate this clipping region, we can use standard QuickDraw functions to get the regions corresponding to the two movie boxes and then subtract one region from the other. We set this region as the main movie's display clipping region using the SetMovieDisplayCl ipRgn function. This is all accomplished in the function QTMT SetMovieClipRegion defined in Listing 3.14. The Movie Toolbox copies the display clipping region that we pass it,
Playing Picture-in-Picture Movies
77
so we can safely dispose of that region, as well as the two intermediate regions we use to calculate the display clipping region, before exiting QTMT_SetMovieEl i pRegi on.
Listing 3.14 Setting the display clipping region of the main movie. OSErr QTMT_SetMovieClipRegion (WindowObject theWindowObject) Rect RgnHandle RgnHandle RgnHandle Appl icationDataHdl
myRect; myMovieRegi on; myPIPRegion; myCl ipRegion; myAppData= NULL;
/ / get the application-specific data myAppData = (Appl i cat i onDataHdl ) QTFrame_GetAppDataFromWi ndowObject (theWi ndowObject) ; i f (myAppData == NULL) return (paramErr) ; myMovieRegion = NewRgn(); myPI PRegion = NewRgn() ; myCl i pRegion = NewRgn() ; GetMovieBox((**theWindowObject). fMovie, &myRect); RectRgn(myMovieRegion, &myRect) ; myRect = (**myAppData). fPIPRect; i f ( I EmptyRect(&myRect)) MaclnsetRect(&myRect, -kPIPBorderSize, -kPIPBorderSize) ; RectRgn (myPIPRegion, &myRect); Di ffRgn(myMovieRegion, myPIPRegion, myClipRegion) ; SetMovi eDi spl ayCl ipRgn( (**theWi ndowObject), fMovi e, myClipRegion) ; Di sposeRgn(myMovieRegion) ; Di sposeRgn(myPI PRegion) ; Di sposeRgn(myCl i pRegion) ; return (noErr) ;
Notice that we call MaclnsetRect inside of QTMT_SetMovieClipRegion to expand the picture-in-picture movie box by a preset amount (kPIPBorderSi ze) before converting that box to a region. We do this to provide a few pixels of space in which to draw a border around the picture-in-picture movie, which
78
Chapter3 Out of Control
gives it a somewhat better appearance. {Listing 3.15 provides the code that draws the border.) There is one final thing worth mentioning: if the main movie were being played using a movie controller component, then we should set the movie display clipping region using the function MCSetClip instead of SetMovieDi spl ayCl ipRgn.
Playing the Picture-in-Picture Movie Now that w e ' v e o p e n e d the picture-in-picture movie and p o s i t i o n e d it w i t h i n the main movie window, we need to start the picture-in-picture movie playing and keep it playing. All the lessons we learned earlier in this chapter apply here as well, so we'll kick things off by preprerolling the picture-in-picture movie, like this: PrePrerolIMovie(myPIPMovie, O, GetMoviePreferredRate(myPIPMovie), gMoviePPRolICompleteProc, (void *)theWindowObject) ;
We're using the same movie preprerolling completion function that we used for the main movie, which prerolls the movie as soon as the preprerolling is completed. But you'll recall from Listing 3.7 that the completion procedure includes a few extra lines that apply only to the picture-in-picture movie: i f (theMovie == (**myAppData).fPIPMovie) { QTMT_SetMovieLoopState(theMovie, kNormalLooping) ; StartMovi e(theMovi e) ;
}
Since we're not going to provide a way for the user to start or stop the picture-in-picture movie, we call QTMT_SetMovieLoopState {defined in Listing 3.11) to set the movie looping state to normal looping. The movie will then keep playing, over and over, until we close the window or replace it with some other picture-in-picture movie. Then, we call StartMovie to start the picture-in-picture movie playing. So the only thing that remains for us to do is task the movie periodically to keep it playing back smoothly. As before, this is something we want to do in our idle routine {QTApp_Idle), where we are already calling MoviesTask on the main movie. We have several options here. First, we could just add these few lines of code to QTApp_Idle, to explicitly task the picture-in-picture movie" i f ((**myAppData).fPIPMovie i= NULL) MoviesTask((**myAppData). fPIPMovie, DoTheRightThing) ;
Playing Picture-in-Picture Movies
79
Or, we could pass the value NULLas the first parameter to MoviesTask, like this: MoviesTask(NULL, DoTheRightThing) ;
When we specify NULL in place of a movie, we're instructing the Movie Toolbox to service all open movies belonging to the application. Remember, however, that we want to draw a border around the picture-in-picture movie. So the option we'll actually choose will be to call an application-defined function" i f ((**myAppData) .fPIPMovie ! = NULL) QTMT_DrawPicture I nPi ctureMovi e (myWindowObject) ; QTMT_DrawPicturelnPictureMovie simply calls MoviesTask on the picture-in-
picture movie and draws a nice-looking border around the movie, as shown in Listing 3.15.
Listing 3.15 Drawing the picture-in-picture movie and its border. OSErr QTMT_DrawPicturelnPictureMovie (WindowObject theWindowObject) { Appl icationDataHdl
myAppData = NULL;
i f (theWindowObject == NULL) return (paramErr) ; myAppData = (Appl i cat i onDataHdl ) QTFrame_GetAppDataFromWi ndowObject (theWi ndowObject) ; i f (myAppData == NULL) return (paramErr) ; / / draw the current frame of the picture-in-picture movie i f ((**myAppData).fPIPMovie [= NULL) MoviesTask((**myAppData). fPIPMovie, DoTheRightThing) ; i f (!EmptyRect(&(**myAppData).fPIPRect)) PenSta te myPenStat e; Rect myRect; myRect = (**myAppData).fPIPRect; / / draw a pseudo-grayscale border Get PenState (&myPenState) ;
80
Chapter 3 Out of Control
{
MacI nsetRect (& (**myAppData). fP I PRect, - kPI PBorderSi ze, -kP I PBorderSi ze) ; RGBForeColor (&gBl ack) ; MacFrameRect (& (**myAppData). fPI PRect) ; MaclnsetRect(&(**myAppData).fPIPRect, 1, 1); RGBForeColor (&gWhi te) ; MacFrameRect (& (**myAppData). fP I PRect) ; MaclnsetRect(&(**myAppData).fPIPRect, 1, 1); RGBForeColor (&gLGray) ; MacFrameRect (& (**myAppData). fP I PRect) ; (**myAppData). fPIPRect = myRect;
}
Set PenState (&myPenState) ;
return (noErr) ;
Moving the Picture-in-Picture Movie Let's finish up by adding a simple e n h a n c e m e n t to our picture-in-picture movie playback capability: w h e n e v e r the user clicks the mouse button and the cursor is inside of the main movie window, let's move the picture-inpicture movie so that its upper-left corner is at the location of the click. Our basic cross-platform QuickTime framework passes mouse button clicks to the application function QTApp_HandleContentCl i ck w h e n e v e r those clicks fall within the w i n d o w ' s content area. So all we need to do is add some code to QTApp_Handl eContentCl i ck.
Moving the picture-in-picture movie rectangle is very simple. First, we need to convert the point that's passed to us in the event record into local coordinates: myPoi nt = theEvent->where; Global ToLocal (&myPoint) ;
T h e n we n e e d to calculate the n e w movie box for the p i c t u r e - i n - p i c t u r e movie. The size of the movie box isn't changing, only its upper-left corner. So we can call MacOffsetRect to figure out w h e r e to place the movie box and SetMovieBox to actually move it. MacOffset Rect (& (**myAppData). fP I PRect, myPoint.h - (**myAppData).fPIPRect.left, myPoint.v - (**myAppData). fPIPRect, top) ; SetMovieBox((**myAppData). fPIPMovie, &(**myAppData). fPIPRect) ;
Playing Picture-in-Picture Movies
81
Finally, we need to recalculate the display clipping region of the main movie, like this" QTMT_SetMovieCl i pRegi on (myWindowObject) ;
The complete procedure for moving the picture-in-picture movie in response to mouse clicks is shown in Listing 3.16. Listing 3.16 Moving the picture-in-picture movie. void QTApp_HandleContentClick (WindowReference theWindow, EventRecord *theEvent)
{
GrafPt r WindowObject Appl icationDataHdl
mySavedPort; myWindowObject = NULL; myAppData = NULL;
Get Port (&mySavedPort) ; MacSetPort (QTFrame GetPortFromWindowReference (theWi ndow)) ; m
myWindowObject = QTFrame_GetWindowObjectFromWi ndow(theWi ndow) ; myAppData = (Appl icationDataHdl)QTFrame_GetAppDataFromWindow(theWindow) ; i f ((myWindowObject == NULL) II (myAppData == NULL)) return; / / move the picture-in-picture movie to the specified location i f ((**myAppData).fPIPMovie l= NULL) { Point myPoint; myPoi nt = theEvent->where; Global ToLocal (&myPoint) ; / / i f the main movie i s n ' t playing, erase the rectangle currently occupied / / by the picture-in-picture movie i f (!QTMT_IsMoviePlaying((**myWindowObject).fMovie)) { Rect myRect; myRect = (**myAppData).fPIPRect; MaclnsetRect (&myRect, -kPIPBorderSize, -kPIPBorderSi ze) ; EraseRect (&myRect) ;
82
Chapter3 Out of Control
MacOffset Rect (& (**myAppData). fP I PRect, myPoint.h - (**myAppData).fPIPRect.left, myPoint.v - (**myAppData). fPIPRect.top) ; SetMovieBox((**myAppData). fPIPMovie, &(**myAppData). fPIPRect) ; QTMT_SetMovieCl i pRegi on (myWindowObject) ; MacSetPort (mySavedPort) ;
Conclusion Keep in mind that the point of this chapter is not that we should be playing movies using the Movie Toolbox alone, without the assistance of movie controllers. Indeed, as we've seen, movie controllers do lots and lots of useful work for us, as well as a good deal of h u m d r u m housekeeping (such as setting the movie rate to 0 w h e n the movie reaches the end). Still, there are occasions w h e n we might need to manage a movie without using a movie controller, and we've seen (fairly exhaustively, I hope) w h a t we need to do to accomplish that. But our larger goal here was to get acquainted with the Movie Toolbox itself, to get a taste of its capabilities and to see how to operate on movies directly.
Conclusion
83
This Page Intentionally Left Blank
The Image
introduction QuickTime is perhaps best known for its ability to play audio and video, but over the years it has gained the ability to handle quite a large number of other types of media, including text, MIDI data, virtual reality, sprite animation, vector-defined images, and streamed audio and video. QuickTime also provides a very powerful set of capabilities for managing still images {or graphics) stored in any of a large number of common image file formats. For instance, we can use QuickTime's application programming interfaces to open and display PICT, TIFF, JPEG, BMP, PhotoShop, GIF, PNG, and FlashPix files [to name only a handful of the supported formats). Moreover, we can use QuickTime to save an image in any one of a number of image formats, including PICT, TIFF, JPEG, BMP, PhotoShop, and PNG. These two aspects of handling images--reading them from files and writing them to files--are handled by parts of QuickTime known as graphics importers and graphics exporters. In this chapter, we'll investigate the most fundamental ways of using graphics importers and exporters to manipulate still images. The most obvious benefit that graphics importers and exporters provide is to insulate our applications from the nitty-gritty details of any particular image file format and image compression scheme. We can use exactly the same routines to open and display a JPEG file as we use to open and display a PICT file. In fact, our application never really needs to know what kind of image file it has opened. We can always determine this information about the image file if we want to, but for most uses that's information we simply don't need. Graphics importers and exporters provide a host of other services. We can change the image matrix used by a graphics importer when drawing an image, thereby drawing the image at a different size or orientation. We can also change the graphics mode used by a graphics importer; this allows us, for instance, to composite the image on top of other images or on top of QuickTime movies. 85
In this chapter, we'll see how to open an image file, read the image from it, and draw that image in a window on the screen. We'll also see how to alter the image matrix to draw the image at different sizes and orientations. Finally, we'll see how to export the image into some other type of image format. To wrap this all up into a neat package, let's set ourselves the goal of replicating as much of the functionality of QuickTime's PictureViewer application as possible. Figure 4.1 shows PictureViewer's Image menu, and Figure 4.2 shows the Image menu of our application, which I'll call QTGraphics. As you can see, these menus are quite similar. The main difference is that QTGraphics adds a check mark in the menu to indicate the current size of the image window. This is a useful feature and it's relatively simple to add, given the way we've chosen to handle our images. Another difference is that QTGraphics does not support the Fill Screen menu command. The reason for this is simply that fitting an image to the current screen size is extremely platform-specific and not terribly enlightening, at least in terms of using graphics importers. So I've left the implementation of that feature as an exercise for the reader. Finally, the Image menu of QTGraphics contains the
86
Figure 4.1
The Image menu from the PictureViewer application.
Figure 4.2
The Image menu from the QTGraphics application.
Chapter 4 The Image
Export item, which PictureViewer places in its File menu (where it more properly belongs). Along the way, we'll take a few small detours to explore some other features of graphics importers and exporters. We'll consider briefly how to handle image files that contain more than one image, and we'll take a quick look at how to distinguish image files from other kinds of files. So let's get started.
Importing Images A graphics import component, or graphics importer, is a software component that provides services for handling still images. Graphics importers were introduced in QuickTime version 2.5 to facilitate opening and drawing images. Subsequent versions of QuickTime have added new graphics importers and expanded the capabilities of existing importers. For instance, QuickTime 4 added the ability to work with files that contain multiple images or multiple image layers (such as FlashPix files or PhotoShop files). It's very simple to open an image file and display the image(s) contained in the file using the application programming interfaces provided by graphics importers. Suppose that we've got a file system specification (that is, an FSSpec) that picks out an image file. Then we can use the QTGraph_ShowImageFromFile function defined in Listing 4.1 to display that image in a window on the screen. Listing 4.1 Opening an image file and displaying its image. void QTGraph_ShowImageFromFile (FSSpec *theFSSpec)
{
GraphicslmportComponent Rect WindowPtr
myImporter = NULL; myRect; myWindow = NULL;
GetGraphicslmporterForFile(theFSSpec, &mylmporter) ; i f (mylmporter ! = NULL) { Graphi cs ImportGetNatura I Bounds(myImporter, &myRect) ; MacOffsetRect(&myRect, 50, 50); myWindow = NewCWindow(NULL, &myRect, NULL, true, noGrowDocProc, (WindowPtr)-lL, true, O) ; i f (myWindow ! = NULL) { GraphicslmportSetGWorld(mylmporter, (CGrafPtr)myWindow, NULL); Graphics ImportDraw (mylmporter) ; Cl oseComponent(mylmporter) ;
}
}
Importing Images
87
QTGraph_ShowlmageFromFi I e first calls the GetGraphics ImporterForFi I e function, which opens the specified image file and determines which graphics import component (if any) can handle the image data contained in that file. If GetGraphicsImporterForFile finds a suitable graphics importer, it returns an instance of that importer in the myImporter parameter. We'll use that graphics importer instance, of type GraphicsImportComponent, in all our subsequent calls to graphics importer functions. The type GraphicsImportComponent is simply a component instance: typedef ComponentInstance
Graphics ImportComponent;
As a result, we shall eventually need to close the component instance by calling CloseComponent {as shown in the final line of Listing 4.1). GetGraphicslmporterForFile uses a fairly straightforward algorithm for deciding what kind of image a file contains. First, on Macintosh systems, it looks at the file's type. For instance, if the specified file is of type 'PI CT', then GetGraphicsImporterForFile opens an instance of the PICT image importer. Otherwise, if it cannot find a suitable importer based on the file's type, GetGraphicsImporterForFile inspects the filename extension, if it has one. So, for example, a file whose name ends with .gi f would be imported as a GIF image. Finally, if neither of these steps reveals the type of the image in the file, then GetGraphicsImporterForFi l e sequentially opens an instance of each available graphics importer and instructs it to examine some of the data in the file to see whether it can handle that kind of data. (Image files often contain a header that helps identify the type of image data contained in them.) This process continues until some importer reports that it can handle the image data or until the list of available importers is exhausted. As you can imagine, this final step (called validating the image data) might take a fair amount of time to complete. If we are fairly certain that either the file type or filename extension will tell us what kind of image file we've got (perhaps because they are files we had previously created), then we can use the function GetGraphics ImporterForFi I eWithFl ags, specifying the kDontUseVal i dateToFindGraphi cslmporter flag to suppress the validation step. For a single file, seeing whether GetGraphicslmporterForFile returns a non-NULL importer--even allowing it to perform the validation step--is a perfectly reasonable way of determining whether that file contains an image (as shown in Listing 4.2). But for a large number of files, using GetGraphicsImporterForFile in this way would be too slow. Later on, we'll see a faster way of finding the image files among a large number of files.
88
Chapter4 The Image
Listing 4.2 Determining whether a file contains an image. Boolean QTUtils IslmageFile (FSSpec *theFSSpec)
{
m
Graphics ImportComponent
myImporter = NULL;
GetGraphicslmporterForFile(theFSSpec, &mylmporter) ; i f (myImporter ! = NULL) Cl oseComponent(mylmporter) ; return(mylmporter ! = NULL);
Let's return to Listing 4.1. If GetGraphicslmporterForFile returns a graphics importer instance to us, then we want to use it to draw the image in a window on the screen. We first call GraphicsImportGetNaturalBounds to determine the natural size of the image, which we shall use to set the size of the image window. GraphicsImportGetNaturalBounds always returns a rectangle whose top-left corner is at the origin--that is, at the point (0, 0). We then offset the rectangle a little bit down and to the right, so that (on Macintosh computers) the window's title bar is not obscured by the menu bar. Then we call NewCWindow to create a window in which we can draw the image. Before we can draw the image into that window, we need to set the graphics port of the graphics importer instance, by calling GraphicsImportSetGWorld (in the same way that, in past chapters, we needed to call SetMovieGWorld to set the graphics port of a movie). Finally, we can draw the image into the window by calling GraphicsImportDraw. Figure 4.3 shows the result of calling QTGraph_ShowImageFromFi l e for the sample image installed in the QuickTime folder. As you might imagine, QTGraph_ShowImageFromFile runs equally well under Windows. Figure 4.4 shows the result of executing it on a Windows computer. So far then, using barely a handful of graphics importer functions, we've opened an image file and drawn it at its natural size in a window on the screen. This is impressive. But of course the QTGraph_ShowImageFromFile function doesn't represent a complete solution for opening and displaying image files. For one thing, the title bar of the image window doesn't have a name, which violates good user interface practice. More importantly, the image won't be redrawn if the image window is wholly or partially obscured by some other window and then uncovered. If we want to replicate the functionality of the PictureViewer application, we need to do a bit more work to make things work just right.
Importing Images
89
Figure 4.3 An image file opened using the QTGraph_ShowlmageFromFile function (Macintosh).
Figure 4.4 An image file opened using the QTGraph_ShowlmageFromFil e function (Windows).
90
Chapter 4 The Image
We could, of course, start from scratch and develop a new image-handling application, but that might be a lot of work. Instead, let's see how easy it would be to modify the QTShell application to handle image files as well as movie files. I think we'll be pleasantly surprised.
Expanding the Application Framework Believe it or not, the basic QTShell application can already open and display image files, without directly using any graphics importer functions. Try it: launch QTShell and select the Open menu item in the File menu; then select an image file that you've got lying around and, voilA, the image opens in a window. How can this be? And more to the point" if the basic application we've developed so far can already open image files, w h y bother with graphics importers? Let's consider first how it is that QTShell can open and display image files. When the user selects the Open item in the File menu, our function QTFrame_0penMovieInWindow calls the function StandardGetFilePreview to display to the user a list of files that our application can open. The types of files displayed in that list are determined by the type list passed to StandardGetFilePreview. QTShell passes just one file type, kQTFileTypeMovie, like this: OSType short StandardFi I eReply
myTypeList = kQTFileTypeMovie; myNumTypes = 1; myRepI y;
StandardGetFi lePreview(NULL, myNumTypes, (ConstSFTypeListPtr)&myTypeList, &myReply) ;
The kQTFileTypeMovie constant tells StandardGetFilePreview to list any kinds of files that QuickTirne can open as a movie. On the Macintosh, this includes all files whose file type is 'MooV', and on Windows it includes all files whose file extension is .mov. On both platforms, this constant also causes StandardGetFi lePreview to list any files that can be translated into the QuickTime movie format using a movie importer, which is a software component that can convert certain kinds of files into movies. For example, QuickTime includes a movie importer that can convert AIFF audio files into
Expanding the Application Framework
91
sound-only QuickTime movies. So AIFF files will be shown in the list of files displayed by StandardGetFilePreview w h e n we include kQTFileTypeMovie in the file t ~ e list. At this point, if the user selects one of the files in the list displayed by StandardGetFi I ePrevi ew, QTShell calls the OpenMovieFi I e and NewMovieFromFile functions to get the movie contained in the selected file. If NewMovieFromFi l e determines that the selected file is not a QuickTime movie file, then it looks for a movie importer that can handle the data in the file. For an image file, NewMovieFromFil e invokes a movie importer that opens the image file (using, you guessed it, a graphics importer) and converts it into a movie. The resulting movie has a default duration of 1/15th of a second. The net result of all this importing and converting is that what began life as a still image is now being treated by QTShell as a movie. In particular, our application framework happily attaches a movie controller bar to the converted image, as shown in Figure 4.5. This provides an easy way to resize the image, to be sure, but it's probably not the behavior w e ' d like our applications to exhibit. After all, the user probably thought that he or she was opening an image file, not a movie file. So we want to expand our basic application framework so that it can handle still images without importing them as movies. That is to say, we want to be able to open and manage still images using the graphics importer routines
Figure 4.5
92
An image file opened by a movie importer.
Chapter 4 The Image
discussed earlier. This involves sprinkling a grand total of just over 20 lines of code here and there in our existing framework files. The first thing we need to do is add a field to the w i n d o w object record associated with each w i n d o w that our application opens. The w i n d o w object record now looks like this: typedef struct { WindowReference fWi ndow; Movie fMovie; Movi eControl ler fControl ler; Graphi cs ImportComponent fGraphi cs Importer; FSSpec fFi I eFSSpec; short fFi leResID; short fFi I eRefNum; Boolean fCanResi zeWindow; Boolean flsDirty; Boolean f l sQTVRMovie; QTVRInstance flnstance; OSType fObj ectType; Handle fAppData; } WindowObjectRecord, *WindowObjectPtr, **WindowObject;
The field fGraphicslmporter holds the graphics importer instance, if any, that is being used to manage the still image in the specified window. If we do things right, then either fGraphicsImporter or fMovie will be non-NULL, but not both. This allows us to tell, by inspecting the w i n d o w object record, whether a w i n d o w contains a still image or a movie. The next thing we need to do is prevent our application from calling OpenMovieFile and NewMovieFromFile on any graphics files we open. We can accomplish this by attempting to open a file as a still image before attempting to open it as a movie. So we'll add these lines to the QTFrame_OpenMovieInWindow function: myErr = GetGraphicsImporterForFile(&myFSSpec, &mylmporter) ; i f (mylmporter ! = NULL) goto gotlmageFi le;
Then we'll save the value r e t u r n e d by GetGraphicslmporterForFile in the new field in the w i n d o w object record, like this" (**myWi ndowObject), fGraphi cs Importer - mylmporter;
The last change we need to make to the QTFrame_OpenMovielnWindow routine is to set the graphics world for the graphics importer"
Expanding the Application Framework
93
myPort = (CGrafPtr)QTFrame_GetPortFromWindowReference(myWindow) ; i f (mylmporter ! = NULL) GraphicslmportSetGWorld(mylmporter, myPort, GetGWorldDevice(myPort)) ;
Elsewhere in our application's source code, we need to make a couple of changes whenever we determine the size of a window. There are two occasions w h e n this happens. First, in the Macintosh code that handles w i n d o w dragging, we need to pass the rectangle enclosing the dragged w i n d o w to the OragAlignedWindow function. Previously, w h e n our w i n d o w s always contained movies, we could call GetMovieBox to get this rectangle. Now, we'll have to call either GetMovieBox or GraphicsImportGetBoundsRect, depending on w h e t h e r the w i n d o w being dragged contains a movie or a still image: i f ((**myWindowObject).fMovie l= NULL) GetMovieBox( (**myWindowObject). fMovie, &myRect); i f ((**myWindowObject).fGraphicsImporter ! = NULL) Graphi cs ImportGetNatural Bounds( (**myWindowObject). fGraphi cs Importer, &myRect) ;
Similarly, w h e n we set the size of a w i n d o w (in our function QTFrame SizeWindowToMovie), we need to distinguish between graphics windows and movie windows. Here we'll call GraphicsImportGetNaturaIBounds if the w i n d o w belongs to a still image and not to a movie: i f (mylmporter l= NULL) { Graphi cs ImportGetNatural Bounds(myImporter, &myMovieBounds) ; goto gotBounds;
}
Notice that our application framework does not attach a grow box or scroll bars to an image w i n d o w and so does not allow the user to resize the image window. The function QTFrame_Si zeWi ndowToMovie (which now, alas, is misnamed) is called for an image w i n d o w only once, w h e n the image is first opened. That's w h y we call firaphi cs ImportfietNatural Bounds to set the image size there. Next, we need to make sure that an image w i n d o w gets redrawn correctly if any part of it gets covered up and then uncovered. Whenever this happens, an update event is issued for the affected w i n d o w (on the Macintosh), or a WM_PAINTmessage is sent to the affected w i n d o w ' s w i n d o w procedure (on Windows). In both cases, we can simply call GraphicslmportDraw to redraw the image, like this:
94
Chapter4 The Image
if
(myWindowObject ! = NULL) i f ((**myWindowObject).fGraphicslmporter ! = NULL) Graphi cs ImportDraw ( ( **myWi ndowObject ). fGraph i cs Importer) ;
The final thing we need to do is remember to close the graphics importer when the window containing the still image is being closed; so we'll add these lines to the function QTFrame CloseWindow0bject: m
i f ((**theWindowObject).fGraphicsImporter ! = NULL) { Cl oseComponent ( (**theWi ndowObject ). fGraphi cs Importer) ; (**theWindowObject).fGraphicsImporter = NULL;
}
That completes the changes we need to make to our basic application framework in order to support still images in addition to movies. (Now wasn't that pleasantly easy?)
Transforming Images Now that we've modified our application framework to handle both image and movie files, we can proceed with our main concern in this chapter, which is to replicate most of the capabilities of the PictureViewer application. Recall that we want to support the selection of items in the Image menu shown in Figure 4.2. Listing 4.3 shows how we'll handle those selections" we'll just call some application-defined function in response to any of the selected menu items.
Listing 4.3 Handlingthe items in the Image menu. Boolean QTApp_HandleMenu (UInt16 theMenultem)
{
WindowObject Boolean
myWindowObject= NULL; mylsHandled = false
myWindowObject = QTFrame_GetWindowObjectFromFrontWi ndow() ; switch (theMenultem) { case IDM HALF SIZE" case IDM NORMAL SIZE" case IDM DOUBLE SLZE: case IDM FILL SCREEN" QTGraph_Scal elmage(myWi ndowObject, theMenuItem) ; mylsHandled = true; break; m
m
Transforming Images
95
case IDM ROTATE LEFT: case IDM ROTATE RIGHT: QTGraph_Rotatelmage(myWindowObject, theMenultem) ; mylsHandled = true; break; case IDM FLIP HORIZcase IDM FLIP VERTQTGraph_Fl i plmage(myWindowObject, theMenultem) ; mylsHandled = true; break; case IDM EXPORT IMAGE: QTGraph_Export Image(myWindowObject) ; mylsHandled = true; break; default. break;
return (mylsHandl ed) ;
So our work will be done once we define the four QTGraph_ functions called in QTApp_HandleMenu. With the exception of QTGraph_ExportImage, which we'll consider later, all these functions operate primarily by manipulating the image matrix, which determines the orientation and size of the image drawn by the graphics importer. So let's take a minute to get acquainted with image matrices.
Working with Image Matrices An image matrix (or image transformation matrix) is a 3-by-3 array of numbers that specifies how to map an image from one two-dimensional coordinate space into another two-dimensional coordinate space. QuickTime defines the Matri xRecord data type for working with matrices: struct MatrixRecord ( Fixed
};
96
Chapter4 The Image
matrix[3] [3];
Pay attention to the fact that the elements of a MatrixRecord structure are of type Fi xed, which represents a fixed-point n u m b e r (that is, a n u m b e r consisting of an integer part and a fractional part, w h e r e there are a fixed number of bytes allotted to each part of the number). In a Fixed data type, the two high-order bytes are the integer part of the fixed-point n u m b e r and the two low-order bytes are the fractional part of the fixed-point number. So, for example, the n u m b e r 1.5 would be represented as 0x00018000, and 2.25 would be represented as 0x00024000. The header file FixMath.h contains a n u m b e r of useful functions for converting between long and fixed-point data types. We'll use the Long2Fix and FixRatio functions to help us specify the appropriate fixed-point values in our image matrices. FixMath.h also defines the constant fixed1, which we'll use instead of calling Long2Fix(1). There are a n u m b e r of standard transformations that we can perform on an image, such as enlarging or shrinking it, rotating it about some point, moving it horizontally or vertically, and skewing it (that is, essentially, grabbing a corner of it and stretching it). Each of these types of transformations is associated with some modification to the 3-by-3 image matrix. For instance, to translate an image horizontally, we need to modify only the [2][0] component of the image matrix. Similarly, to change the width of an image, we need to modify only the [0][0] component of the image matrix. To save us from having to r e m e m b e r all this, QuickTime provides a n u m b e r of functions that we can use to set up a matrix to perform some desired transformation. For present purposes, we shall use the Scal eMatri x, RotateMatri x, TranslateMatrix, and SetldentityMatrix functions. (The identity matrix is the matrix that transforms an image into the identical image; in other words, the identity matrix leaves the image unchanged.) Let's consider a few concrete examples here. Suppose that we have an image whose top-left corner is located at the point (0, 0). Then we can double the width and height of that image by executing this line of code:
ScaleMatrix(&myMatrix, Long2Fix(2), Long2Fix(2), O, 0); The last two parameters specify the horizontal and vertical coordinates of the anchor point of the scaling {that is, the point that stays fixed during the scaling operation). For simplicity, we'll always anchor our scaling to the point (0, 0), which we'll maintain as the top-left corner of the image. Next, let's see how to flip an image horizontally. It turns out that we can do this simply by scaling the image by a factor of -1 in the horizontal direction. This effectively just flips the image across the y-axis"
ScaleMatrix(&myMatrix, Long2Fix(-1), fixed1, O, 0);
Transforming Images
97
Figure 4.6 Scalingand translating to flip an image horizontally.
But there is a complication here. If we flip the image across the y-axis in this manner, the top-left corner of the image is no longer located at (0, 0). Instead, it's located at (-n, 0), where n is equal to the width (in pixels) of the image. If we want the top-left corner of the image to remain at (0, 0), then we need to translate the image back to the right after we've flipped it horizontally:
ScaleMatrix(&myMatrix, Long2Fix(-1), fixed1, O, 0); TranslateMatrix(&myMatrix, Long2Fix(myRect.right), O) ; (Here we assume that myRect is the rectangle that encloses the image.) Note that the matrix functions ScaleMatrix and TranslateMatrix modify the matrix they are passed so that it incorporates the indicated transformation. In other words, ScaleMatrix adds (or in mathematical jargon, concatenates) the specified scaling to the existing transformations in myMatrix. So calling ScaleMatrix and then TranslateMatrix results in a matrix that scales and then translates the image. With matrices, the order of operations is important; doing a translation before a scaling almost always results in a different image than doing the scaling before the translation. Figure 4.6 shows the result of scaling and then translating an image to flip it horizontally.
Flipping Images Now we've got the important ammunition that we need to handle the Flip Horizontal and Flip Vertical m e n u items in the Image menu. In a nutshell, we need to get the current image matrix, concatenate a scale matrix and a translate matrix to that image matrix, and then set the revised matrix as the new image matrix. We can use the GraphicslmportGetMatrix function to get
98
Chapter 4 The Image
the current image matrix and the Graphi cslmportSetMatri x function to set the
image matrix. Recall, however, that we also need to know the rectangle enclosing the image, so that we can translate the flipped image horizontally or vertically to return its top-left corner to the origin. A graphics importer component provides the GraphicslmportGetBoundsRect function, which returns the current size of the image after the existing image matrix has been applied to it. The rectangle returned by GraphicsImportGetBoundsRect always has its l e f t and top fields set to O, so we can use the right and bottom fields to determine the width and height of the image. Listing 4.4 shows our QTGraph_FlipImage function.
Listing 4.4 Flipping an image horizontally or vertically. OSErr QTGraph_Fliplmage (WindowObject theWindowObject, UInt16 theFlipDirection)
{
Graphi cslmportComponent Matri xRecord Rect OSErr
mylmporter = NULL; myMatri x; myRect; myErr = paramErr;
i f (theWindowObject == NULL) goto bai I ; mylmporter = (**theWindowObject). fGraphicslmporter; i f (mylmporter == NULL) goto bai I ; myErr = GraphicslmportGetMatrix(mylmporter, &myMatrix) ; i f (myErr ! = noErr) goto bai I ; myErr = GraphicslmportGetBoundsRect(mylmporter, &myRect) ; i f (myErr ! = noErr) goto bai I ; switch (theFlipDirection) { case IDM FLIP HORIZ: ScaleMatrix(&myMatrix, Long2Fix(-1), fixedl, O, 0); TranslateMatrix(&myMatrix, Long2Fix(myRect.right), O) ; break; case IDM FLIP VERT: ScaleMatrix(&myMatrix, fixed1, Long2Fix(-1), O, 0); TranslateMatrix(&myMatrix, O, Long2Fix(myRect.bottom)) ; break;
Transforming Images
99
default: return (paramErr) ;
myErr = GraphicslmportSetMatrix(mylmporter, &myMatrix) ; i f (myErr ! = noErr) goto bai I ; myErr = GraphicslmportDraw(mylmporter) ; bail 9 return (myErr) ;
}
Notice that we call GraphicslmportDraw before exiting QTGraph_Fl i plmage so that the image in the w i n d o w gets r e d r a w n using its new matrix.
Rotating Images Let's move on to consider the Rotate Left and Rotate Right m e n u items in the Image menu. In general, we can handle them in pretty m u c h the same way that we handled the Flip c o m m a n d s in OTGraph_F] i pImage: get the current image matrix, construct a new matrix for the desired rotation, add a translation matrix to return the top-left corner of the rotated image to the origin, and then set the new image matrix (Figure 4.7). But there is one important difference here" the size of the image changes w h e n we rotate it 90 ~ clockwise or counterclockwise (unless of course the original image is square). So we'll need to resize the w i n d o w that contains the image before we redraw the image.
Figure 4.7
1lOll
Rotating and translating to rotate an image clockwise.
Chapter 4 The Image
This complication is actually fairly easy to handle. We simply need to call GraphicsImportGetBoundsRect a second time, after we've set the new image matrix. GraphicsImportGetBoundsRect will then return to us the rectangle that encloses the image, after the new matrix has been applied to the image. This gives us the new height and width for our image window, which we can resize by calling SizeWindow. As before, we'll finish up by calling GraphicsImportDraw to redraw the image in its new rotated orientation. Listing 4.5 shows our function QTGraph_RotateImage for rotating images.
Listing 4.5 Rotating an image. OSErr QTGraph Rotatelmage (WindowObject theWindowObject, UInt16 thelmageRotation)
{
GraphicslmportComponent MatrixRecord Rect OSErr
mylmporter = NULL; myMatrix; myRect; myErr = paramErr;
i f (theWindowObject == NULL) goto bai I ; mylmporter = (**theWindowObject).fGraphicslmporter; i f (mylmporter == NULL) goto bai I ; myErr = GraphicslmportGetMatrix(mylmporter, &myMatrix) ; i f (myErr ! = noErr) goto bai I ; myErr = GraphicslmportGetBoundsRect(mylmporter, &myRect) ; i f (myErr ! = noErr) goto bai I ; switch (thelmageRotation) { case IDM ROTATE LEFT: RotateMatrix(&myMatrix, Long2Fix(-90), O, 0); TranslateMatrix(&myMatrix, O, Long2Fix(myRect.right)) ; break; case IDM ROTATE RIGHT: RotateMatrix(&myMatrix, Long2Fix(90), O, 0); TranslateMatrix(&myMatrix, Long2Fix(myRect.bottom), 0); break; default: return (paramErr) ;
Transforming Images 10'11
myErr = GraphicslmportSetMatrix(mylmporter, &myMatrix) ; i f (myErr != noErr) goto bai I ; myErr = GraphicslmportGetBoundsRect (mylmporter, &myRect) ; i f (myErr != noErr) goto bai I ; / / set the new window size Si zeWi ndow(QTFrame_GetWindowFromWindowReference ( (**t heWindowObject). fWi ndow), myRect.right, myRect.bottom, false) ; myErr = GraphicslmportDraw(mylmporter) ; bail 9 return (myErr) ;
}
Scaling Images The final image transformation we want to handle is scaling the image to a new size. As with rotating, scaling to a new size requires us to worry about setting a new w i n d o w size after we've updated the image matrix, but we can use the code we just developed to handle that. The added complication we face now is that the Half Size, Normal Size, and Double Size m e n u items specify absolute sizes, not relative sizes. That is to say, selecting the Half Size menu item should result in an image that is half its natural size, not half the current size of the image. So we can't just concatenate a scaling matrix onto the existing image matrix. There are several ways we could handle this complication. For my money, the most natural way is to keep track of the current size of the image and then perform the appropriate relative scaling w h e n we get a request to change its size. So we'll put the single field fCurrentSize into the application data structure associated with each window: typedef struct ApplicationDataRecord { UI nt 16 fCurrentS i ze; } Appl icationDataRecord, *Appl icationDataPtr, **Appl icationDataHdl ;
Then we can handle the Half Size m e n u c o m m a n d like this" case IDM HALF SIZE: i f ((**myAppData) .fCurrentSize == IDM_NORMAL_SIZE) ScaleMatrix(&myMatrix, FixRatio(1, 2), FixRatio(1, 2), O, 0);
102
Chapter4 The Image
i f ((**myAppData) .fCurrentSize == IDM_DOUBLE_SlZE) ScaleMatrix(&myMatrix, FixRatio(1, 4), FixRatio(1, 4), O, 0); break;
As you can see, we scale the image relatively by a factor of one-half or one-fourth, depending on whether the image currently is at its normal size or is twice its normal size. (Of course, if the image is already at half its normal size, we don't add any new scaling to the image matrix.) Listing 4.6 shows our complete function for handling image size changes.
Listing 4.6 Changing the size of an image. OSErr QTGraph_Scalelmage (WindowObject theWindowObject, UInt16 thelmageSize) Appl i cationDataHdl Graphi cs ImportComponent MatrixRecord Rect OSErr
myAppData = NULL; mylmporter = NULL; myMatrix; myRect; myErr = paramErr;
myAppData = (AppI i cat i onDataHdl ) QTFrame_GetAppDataFromWi ndowObject (theWi ndowObject) ; i f (myAppData == NULL) goto bai I ; mylmporter = (**theWindowObject). fGraphicsImporter; i f (mylmporter == NULL) goto bai I ; myErr = GraphicsImportGetMatrix(mylmporter, &myMatrix) ; i f (myErr ! = noErr) goto bai I ; switch (thelmageSize) { case IDM HALF SIZE" i f ((**myAppData) .fCurrentSize == IDM_NORMAL_SIZE) ScaleMatrix(&myMatrix, FixRatio(1, 2), FixRatio(1, 2), O, 0); i f ((**myAppData) .fCurrentSize == IDM_DOUBLE_SIZE) ScaleMatrix(&myMatrix, FixRatio(1, 4), FixRatio(1, 4), O, 0); break; case IDM NORMALSIZE" / / reset everything Set Identi tyMatri x (&myMatri x) ; break; n
Transforming Images 103
case IDM DOUBLESLZE: i f ((**myAppData).fCurrentSize == IDM HALF SLZE) ScaleMatrix(&myMatrix, Long2Fix(4), Long2Fix(4), O, 0); n
i f ((**myAppData).fCurrentSize == IDM_NORMAL_SIZE) ScaleMatrix(&myMatrix, Long2Fix(2), Long2Fix(2), O, 0); break; case IDM FILL SCREEN: / / l e f t as an exercise to the reader break; n
D
default: return (paramErr) ; (**myAppData).fCurrentSize = thelmageSize; myErr = GraphicslmportSetMatrix(mylmporter, &myMatrix) ; i f (myErr l= noErr) goto bai I ; myErr = GraphicslmportGetBoundsRect(mylmporter, &myRect) ; i f (myErr l= noErr) goto bai I ; Si zeWindow(QTFrame_GetWindowFromWindowReference ( (**t heWindowObject). fWi ndow), myRect.right, myRect.bottom, false) ; myErr = GraphicslmportDraw(mylmporter) ; bail: return (myErr) ;
}
Notice that the code for handling the Normal Size menu item is refreshingly simple" it just calls SetIdentityMatrix to reset the image matrix to the identity matrix. This has the effect of setting the image to its natural size and orientation, thereby undoing any scaling, rotation, and flipping the user may previously have performed. As mentioned earlier, one advantage of keeping track of the current size of the image is that we have an easy way to add a check mark to the correct item in the Image menu. Our function QTApp_AdjustMenus contains a few lines that look essentially like this:
104
Chapter 4 The Image
mySize = (**myAppData).fCurrentSize; for (myltem = IDM HALF SIZE; myltem <= IDM FILL SCREEN; myltem++) QTFrame_SetMenuI temCheck(myMenu, myl tem, mySize == myl tem) ;
Working with Multi-image Files Before moving on to consider graphics exporters, let's take a minute to see how we can work with files that contain more than one image. As I mentioned earlier, this capability was introduced in QuickTime 4. In particular, QuickTime 4 added the ability to import (but not export) graphics in the FlashPix format defined by the Digital Imaging Group. Also, QuickTime 4 added support for opening and displaying multiple images contained in PhotoShop files [which store layers as separate images in a single file). Earlier versions of QuickTime could open PhotoShop files but imported all the layers into a single image. You can determine how many images a particular graphics file contains by calling the GraphicslmportGetlmageCount function. And you can select which of those images is displayed by calling the GraphicslmportSetlmageIndex function. Listing 4.7 illustrates how to display all the images contained
in a specified file. The QTGraph_ShowImagesFromFi1e function is simply a modified version of QTGraph_ShowImageFromFile (defined in Listing 4.1) that loops through all the images in the image file, pausing a few seconds to display each image. Listing 4.7 Displaying all images in a graphics file. void QTGraph_ShowlmagesFromFile (FSSpec *theFSSpec)
{
GraphicslmportComponent Rect unsigned long WindowPtr
mylmporter = NULL; myRect; myCount, mylndex, mylgnore; myWindow = NULL;
GetGraphicslmporterForFile(theFSSpec, &mylmporter) ; i f (mylmporter i= NULL) { / / determine how many images are in the specified f i l e Graphi cs ImportGet ImageCount(myImporter, &myCount); / / loop through all images in the image f i l e , drawing each into a window for (mylndex = 1; mylndex <= myCount; mylndex++) { / / set the image index we want to display GraphicslmportSetlmagelndex(mylmporter, mylndex) ;
Working with Multi-image Files 105
Graphics ImportGetNatural Bounds(myImporter, &myRect); MacOffsetRect(&myRect, 50, 50); myWindow = NewCWindow(NULL, &myRect, NULL, true, noGrowDocProc, (WindowPtr)-lL, true, O) ; i f (myWindow ! = NULL) { GraphicslmportSetGWorld(mylmporter, (CGrafPtr)myWindow, NULL); Graphics ImportDraw(mylmporter) ; Delay(120, &mylgnore); / / wait 2 seconds Di sposeWi ndow(myWindow) ;
}
C1oseComponent(mylmporter) ;
Exporting Images A graphics export component, or graphics exporter, is a software component that provides services for saving a still image in a new format. While graphics importers allow us to read in images that are stored in any of a wide number of formats, graphics exporters allow us to write out images into any of a fairly large number of formats. Currently, the formats supported by QuickTime's graphics exporters are a subset of those supported by the graphics importers; but in theory there is nothing (other than programming resources and possibly also licensing restrictions) preventing the importers and exporters from supporting the same formats. Graphics exporters are relative newcomers to the QuickTime scene, having been introduced in their full glory only in version 4. But QuickTime has supported some means of exporting images from as early as version 2.5, which included the GraphicslmportSaveAsPicture and GraphicsImportSaveAsQuickTimeImageFile functions. QuickTime 3.0 added a handful of additional functions, including GraphicsImportExportImageFile and GraphicsImportDoExportImageFileDialog (which we'll investigate further shortly). These functions provided much needed services for converting images from one format to another, but they did not provide a very extensive set of capabilities for customizing the export process or the features of the exported images. The graphics exporters introduced in QuickTime 4 expanded the available export formats and greatly increased the ability of applications to manipulate images during export. Moreover, the newly defined interface for graphics exporters opened the door for third parties to create custom graphics exporters.
106
Chapter4 The Image
Here we'll take a look at two very basic ways of exporting image files. First we'll show how to use GraphicsImportDoExportImageFileDialog to present a standard export image dialog box to the user. Then we'll take a quick look at exporting images without involving the user, first using Graphi csImportExportImageFi ]e and then using the graphics exporter APIs.
Exporting Images Using the Export Image Dialog Box If we're running PictureViewer and have opened an image file, we can select the Export item in the File menu to save the image in a new format. When we select that item, we are presented with the export image dialog box shown in Figure 4.8. As you can see, this dialog box allows you to select an output filename and location, a file type, and some image format options. For instance, if we select the JPEG format for the exported file and click the Option button, we'll see the dialog box that's shown in Figure 4.9. It's actually quite easy to add this capability to our QTGraphics application. QuickTime provides the GraphicslmportDoExportlmageFileDialog function, which we can use to display the export image dialog box and handle all the low-level details of creating the output file and writing the appropriate image data to that file. Indeed, GraphicsImportDoExportImageFileDialog even takes the trouble to append the correct filename suffix to the output file name {although the user can change or remove that suffix, if desired).
Figure 4.8 The export image dialog box.
Exporting Images 107
Figure 4.9 An options dialog box. Graphics ImportDoExport ImageFi I eDi al og takes seven parameters. The first, of course, is the graphics importer that we are using to manage the image to be exported in a new format. The second parameter is a file system specification that indicates a suggested location and name for the output file. We'll have QTGraphics pass in the specification of the image that's open in the window (that is, the file being exported). Since presumably the user will select a new image format and GraphicsImportDoExportImageFileDialog will adjust the filename suffix automatically, we shouldn't run into any danger of overwriting the existing image file. The third parameter to Graphi csImportDoExportImageFi l eDi a log is a Pascal string that is displayed as a prompt in the export image dialog box. Like Pico tureViewer, we'll pass in the string "Save Image As:". The fourth parameter is a universal procedure pointer (UPP) to a modal dialog filter function, which can intercept events passed to the dialog box. In QTGraphics, we'll pass a UPP to the function QTGraph_ExportImageFileDial ogEventFilter, which in turn calls the filter function QTFrame StandardModalDialogEventFilter defined in the file MacFramework.c. The important work done by QTFrame_StandardModalDialogEventFilter is to look for update events that are targeted at one of our application's windows; if it finds any, it calls QTFrame_HandleEvent to dispatch the update event to the correct movie or image window. In practice, this means that any part of the movie or image window that is obscured by some other window and then uncovered will be redrawn, even if the export image dialog box is still displayed on the screen. It's interesting to note that PictureViewer does not seem to have a modal dialog filter function installed here, since it doesn't redraw its image windows while the export image dialog box is displayed. (On Windows, update messages are sent directly to the affected window, so we don't need to install a filter function here; if you look at the QTGraph_ExportImage-
108
Chapter 4 The Image
FileDialogEventFilter function, you'll see that on W i n d o w s it simply returns fa] se to indicate that it didn't handle the event.) The last three parameters to firaphicsImportDoExportImageFileDialog are used to communicate back to its caller some information about the newly created file. Since we don't care about that information, we'll just pass NULL in all three parameters. Listing 4.8 shows our function QTfiraph_ExportImage for exporting images.
Listing 4.8 Exporting an image into a user-selected format. OSErr QTGraph_ExportImage (WindowObject theWindowObject)
{
Graphics ImportComponent StringPtr Modal Fi I terYDUPP OSErr
mylmporter = NULL; myPrompt = QTUti I s ConvertCToPascal String(kExportlmagePrompt) ; myFi I terProcUPP = NULL; myErr = paramErr; m
i f (theWindowObject == NULL) goto bai I ; mylmporter = (**theWindowObject). fGraphicslmporter; i f (mylmporter == NULL) goto bai I ; myFi I terProcUPP = NewModalFi I terYDProc (QTGraph_Export ImageFi I eDi al ogEventFi I ter) ; myErr = Graphi cs ImportDoExport ImageFi I eDi al og ( mylmporter, &(**theWi ndowObject). fFi I eFSSpec, myPrompt myFi I terProcUPP, NULL, NULL, NULL) ; bail: Di sposeRout i neDescri ptor (myFi I terProcUPP) ; free (myPrompt) ; return (myErr) ;
Exporting Images Directly Sometimes we don't want to use the export image dialog box to help us export an image to a file. For instance, the user might want to convert a large
Exporting Images '1109
number of image files from one format to another, using the same settings for all the conversions. In this case, it would be tedious, at best, for the user to have to configure the desired settings in the export image dialog box for each image. Luckily, QuickTime supports more direct ways of exporting files. We can use the graphics exporter functions introduced in QuickTime 4, or {if we want our application to run in versions of QuickTime as early as 3.0) we can use the GraphicslmportExportlmageFile function. GraphicsImportExportImageFile is very easy to use and requires virtually no setup. We need to pass it the graphics importer instance attached to the image that we want to export, along with the desired output file format and a file system specification that indicates the output file. Here's how we might export an image to a JPEG file:
GraphicslmportExportlmageFile(mylmporter, kQTFileTypeJPEG, O, &myExportedFile, smSystemScript) ; The third parameter here specifies the output file's creator; by passing 0 in that parameter, we get the default creator for the specified file type. More often than not, the default creator is set to the PictureViewer application. The fourth parameter specifies the output file, which is created by GraphicsImportExport ImageFi le if it doesn't already exist. If the file does already exist, its data is replaced by the image being exported. When exporting the image data to the output file, GraphicsImportExportImageFi l e applies the current image matrix to the source image. This generally means that the exported image looks just like the image currently displayed on the screen. The only exception to this is when the top-left corner of the image is not located at (0, 0); in that case, GraphicsImportExportImageFi l e temporarily adjusts the image matrix so that the resulting image has a top-left corner that is located at (0, 0). To export an image directly using the graphics exporter programming interfaces, we first need to open an instance of a graphics exporter. That is to say, we need to open a component whose component type is GraphicsExporterComponentType and whose component subtype is the file type of the exported image file. For instance, to export an image as a JPEG file, we could call 0penADefaul tComponent like this:
OpenADefaultComponent(Graphi csExporterComponentType, kQTFiI eTypeJPEG, &myExporter) ; If OpenADefaultComponent finds a graphics export component that can handle JPEG files, it returns an instance of that exporter to us in the myExporter parameter. We'll use that instance in all of our subsequent calls to the graphics exporter.
110
Chapter4 The Image
Now that we've found a suitable graphics exporter, we need to do three things before we can call the GraphicsExportDoExport function to do the actual exporting. First, we need to tell the graphics exporter where to get the image data to export. Since our image is being handled by a graphics importer, we can call GraphicsExportSetlnputGraphicsImporter to tell the
exporter to use the importer as the source of the image data. Second, we need to tell the graphics exporter where to put the exported data. For the present, we'll give it a file, by calling firaphicsExportSetOutputFile. Finally, we might need to change some of the default export settings. For instance, the graphics exporter automatically gives an appropriate file type and creator to any files exported on Macintosh systems; if those settings are not appropriate, we could call GraphicsExportSetOutputFi l eTypeAndCreator to set some other tylae or creator. In the QTGraph_ExportlmageWithoutDialog function, defined in Listing 4.9, we'll explicitly set the output image quality to be codecNormal Qual i ty.
Listing 4.9 Exporting an image into a specified format. OSErr QTGraph_ExportlmageWithoutDialog (WindowObject theWindowObject, OSType theType) Graphics ImportComponent Graphi csExportComponent FSSpec StringPtr OSErr
mylmporter = NULL; myExporter = NULL; myExportedFi I e; myName = QTUtils ConvertCToPascaIString("temp") ; myErr = paramErr;
i f (theWindowObject == NULL) goto bai I ; myImporter = (**theWi ndowObject). fGraph i cs Importer; i f (mylmporter == NULL) goto bai I ; / / create an FSSpec for the f i l e containing the exported image myErr = FSMakeFSSpec(O, O, myName, &myExportedFile); i f ((myErr != noErr) && (myErr l= fnfErr)) goto bai I ; / / get a graphics exporter of the desired type myErr = OpenADefaultComponent(GraphicsExporterComponentType, theType, &myExporter) ; i f (myErr ! = noErr) goto bai I ; myErr = Graphi csExportSetlnputGraphi cslmporter(myExporter, mylmporter) ; i f (myErr ! = noErr) goto bai I ;
Exporting Images 111
myErr = GraphicsExportSetOutputFile(myExporter, &myExportedFile) ; i f (myErr I = noErr) goto bail ; myErr = Graphi csExportSetCompress i onQual i ty (myExporter, codecNormal Qual i ty) ; i f (myErr I= noErr) goto bail ; myErr = GraphicsExportDoExport(myExporter, NULL) ; bail: i f (myExporter ! = NULL) Cl oseComponent (myExporter) ; free (myName) ; return (myErr) ;
One thing worth noticing here is that (just like GraphicsImportExportImageFile) GraphicsExportDoExport will create the specified output file if it doesn't already exist. So we don't need to call FSpCreate to create the file picked out by myExportedFi l e.
Finding Image Files Before we close up shop, let's return to a topic we touched on earlier, namely, how to list to the user the files that our application is capable of opening. Recall that the QTShell application passes to StandardGetFilePreview the single file type kQTFileTypeMovie; this results in a list that includes all files that are of type 'MooV' or are of a type that can be imported by one of QuickTime's movie importers. As we saw, this list will also include all image files that QuickTime can open, since QuickTime includes a movie importer that can import image files. This is well and good for an application that wants to be able to open both movie files and image files. But it's less satisfactory for an application like PictureViewer that is concerned solely with image files. However, StandardGetFilePreview supports another constant, kQTFileTypeOuickTimelmage, which tells it to list all files that are of type 'qti f' or are of a type that can be imported by one of QuickTime's graphics importers. If I were a betting man, I'd wager a hefty sum that PictureViewer includes some source code that looks a lot like this:
112
Chapter4 The Image
OSType StandardFi I eReply
myTypeList = kQTFiI eTypeQuickTimeImage; myReply;
StandardGetFilePreview(NULL, 1, (ConstSFTypeListPtr)&myTypeList, &myReply);
For an application like QTShell or QTGraphics that knows how to handle both movies and images, we can specify that we want both movie files and image files listed in the file-opening dialog box, like this: OSType StandardFi I eReply
myTypeList [] = {kQTFileTypeMovie, kQTFi I eTypeQui ckTi meImage} ; myReply;
StandardGetFi lePreview((Fi leFi I terUPP)theFi I terProc, 2, (ConstSFTypeLi stPtr)myTypeLi st, &myReply) ;
As we've seen, adding kQTFileTypeQuickTimelmage here is overkill, but it's worth putting it in, if only to remind us that we're going to get image files as well as movie files. But now a problem presents itself. What if we want to make our application compatible with Carbon, the set of programming interfaces and libraries t h a t s u p p o r t execution on both Mac OS X and certain versions of the "classic" Macintosh operating system (namely, Mac OS 8 and Mac OS 9)? The Standard File Package is no longer available to Carbon applications, and its replacement, Navigation Services, does not provide any special processing for the constants kQTFi l eTypeMovie and kOTFi l eTypeOuickTimelmage. To make a long story short, what we need to do is provide a UPP to a l~le filter function when calling NavGetFi l e, the Navigation Services replacement for StandardGetFilePreview. This filter function is passed information about each file that is a candidate for being listed in the file-opening dialog box; the filter should return true for each file that our application wants to appear in that list of files. Suppose for the moment that we want our application to be able to open only image files. Should we just call the QTUtils_IslmageFile function defined in Listing 4.2 to determine whether a candidate file should be listed? The answer, as I've already hinted earlier, is no. In fact, that test would work just fine in separating the image files from the non-image files, but it would take far too long to do so. (Even on a relatively fast machine, using QTUtils_IslmageFile in our filter function to pick out image files would take several minutes.) A better strategy is first to build up a list of all the file types that we want our application to be able to open and then, in the file filter function called by NavGetFile, simply look to see whether a candidate file has a type that's in that list. This is the strategy adopted by QTShell and its descendants,
Finding Image Files 113
including QTGraphics. To build up this list of file types, we can iterate through all available graphics importers and ask them what kinds of file types they can handle. Listing 4.10 shows a part of a function QTFrame_AddComponentFileTypes that does this, using Component Manager functions to find all the available graphics import components.
Listing 4.10 Finding all image file types. ComponentDescri pt i on ComponentDescri pt i on Component
myFindCompDesc : {0, O, O, O, 0}; mylnfoCompDesc : {0, O, O, O, 0}; myComponent = NULL;
myFi ndCompDesc,componentType = Graphi cs ImporterComponentType; myFi ndCompDesc.componentFl ags = O; myFi ndCompDesc,componentFI agsMask = movie ImportSubType I s Fi I eExtens i on; myComponent = Fi ndNextComponent (myComponent, &myFindCompDesc) ; while (myComponent ! = NULL) { GetComponentlnfo(myComponent, &mylnfoCompDesc, NULL, NULL, NULL); gVal i dFi I eTypes [*theNext Index] = mylnfoCompDesc, componentSubType; *theNextlndex += 1; myComponent = Fi ndNextComponent (myComponent, &myFindCompDesc) ;
Notice that the componentFlags and componentFlagsMask fields of the component description that we're using to find graphics importers are set up to exclude any graphics importers whose subtype is a file extension. That's because we want to find only those importers whose subtype is a Macintosh file type. (One consequence of this, however, is that our file filter function will exclude files that don't have a recognized file type but do have a recognized filename extension. So our Navigation Services code doesn't quite offer the full functionality of the Standard File Package version.) Once we've built the list of image types that the available graphics importers can handle, we can use that list inside of our file filter function. We don't need to go into the details of that here; if you're interested, take a look at the function QTFrame_GetOneFileWithPreview in the source code file ComFramework. c.
Conclusion We've actually accomplished quite a lot in this chapter. We've seen how to make sure that image files appear in the list of files displayed by the fileopening dialog box (either using StandardGetFilePreview or NavGetFile).
114
Chapter 4 The Image
We've learned how to find a graphics importer capable of opening any of those files and how to use a graphics importer to draw an image in a window on the screen. We've also learned how to perform some simple transformations on an image, by altering the image matrix used by the graphics importer. Finally, we've investigated several methods of exporting images into new image formats. So we've successfully managed to upgrade our existing QTShell application into QTGraphics, our poor man's PictureViewer. Nevertheless, we've barely even begun to cover the many capabilities provided by graphics importers and exporters. If you take a glance into ImageCompression.h (a standard header file provided by QuickTime, where the graphics importer and exporter functions are declared), you'll encounter a large number of functions that we haven't mentioned at all. There are graphic importer functions that allow us to work with data references instead of files (so we could open and display images addressed using URLs). There are functions that allow us to get and set the image clipping region, the source and destination rectangles, and the image quality. With graphics exporters, the situation is even more daunting; of the six dozen graphics exporter functions declared in ImageCompression.h, we have discussed exactly four of them here. Of course, this is only good news, since it means that QuickTime provides us with a very wide array of image importing and exporting services. As our image-handling needs grow, it's likely that QuickTime already provides services we can use to manage those needs.
Conclusion 115
This Page Intentionally Left Blank
In and Out
Introduction In the previous chapter, we looked at graphics importers and exporters, which allow us to read still images from files, draw those images, and save the images in new image formats. In this chapter, w e ' r e going to take a look at movie importers and exporters, which allow us to convert various kinds of data to and from the QuickTime movie format. A movie importer reads data and converts it into a QuickTime movie, and a movie exporter writes QuickTime movie data into some other format. For example, we can use a movie importer to read an AVI file and convert it into the QuickTime movie format. At that point, we can display the movie in a w i n d o w and attach a movie controller to it, exactly as if the original data had been stored in the QuickTime file format. Conversely, we can use a movie exporter to save QuickTime movie data as an AVI file. Because movie importers and exporters allow us to change the format of movie data, they are also called movie
data exchange components. Originally, movie importers and exporters were mainly intended to read and write data stored in files that are not QuickTime files. We can, however, use movie data exchange components for additional purposes. In particular, we can use movie exporters to change the compression format or compression settings of a QuickTime file. We can also use movie exporters to add hint tracks to a movie so that the movie can be streamed over a network. In this case, the existing movie data isn't actually being converted to some other format; rather, new tracks are being added to the movie to enable the movie data to be efficiently broken up into packets that can be sent out over a network. In this chapter, we'll begin by considering how to export a movie into a new format. We'll see how to do this in two ways, first allowing the user to select any available export format and then restricting the export format to a particular format. Next, we'll investigate how to import nonmovie files as
117
movies. Finally, we'll learn how to use movie progress functions, which the Movie Toolbox calls w h e n an operation {such as importing or exporting a movie} promises to take a significant a m o u n t of time.
Exporting Movies To export a movie is to convert it into a new format or to change the movie data in some other way. QuickTime provides a n u m b e r of functions that we can use to export a movie. By far the easiest to use is the ConvertMovi eToFile function, whose declaration (in Movies .h) looks essentially like this: OSErr ConvertMovieToFile ( Movie Track FSSpec * OSType OSType Scri ptCode short * long ComponentInstance
theMovie, onlyTrack, outputFi le, fi I eType, creator, scri ptTag, resID, flags, userComp) ;
Most of these parameters have pretty obvious uses: theMovie, of course, is the movie we want to export, and outputFile points to a file system specification for the file we want the converted movie data to be put into. The fileType and creator parameters are the desired file type and file creator code of the destination file, and scriptTag is the script into which the movie should be converted. A few of these parameters are less obvious. The onlyTrack parameter specifies which track in the source movie is to be converted; we'll always specify NULL for this parameter to have ConvertMovieToFile convert all the tracks in the movie. The reslD parameter points to some storage that will receive the resource ID of the source movie; we w o n ' t need this information, so once again we'll always pass NULLfor this parameter. The userComp parameter specifies which movie export component should be used to perform the data conversion. If we want the movie to be exported into a specific format, we can open the appropriate movie export component and pass the component instance in this parameter. On the other hand, if we want to display a dialog box that allows the user to select the desired output format, then we can pass NULLin this parameter. In that case, we should also specify 0 in the fi l eType parameter (since of course we don't yet know w h a t the file type of the output file should be).
118
Chapter 5 In and Out
Finally, the flags parameter specifies a 32-bit value whose bits encode information governing how the movie conversion should proceed. Our first example of using ConvertMovieToFile will use this line of code to configure the flags parameter: myFlags = createMovieFileDeleteCurFile I showUserSettingsDialog I movieFileSpecVal id I movieToFileOnlyExport; The createMovieFileDeleteCurFile nag tells ConvertMovieToFile to delete any existing file specified by the outputFile parameter. The showUserSettingsDialog flag indicates that ConvertMovieToFile should display a movie export dialog box, shown in Figure 5.1. If the showUserSettingsDialog flag is set, then the movieFileSpecValid flag indicates that the name specified in the name field of the file system specification pointed to by the outputFi l e parameter should be the name initially displayed in the export dialog box. Also, if showOserSettingsBialog is set, then the movieToFileOnlyExport flag indicates that the user should be allowed to select only from among the output formats supported by any available movie export components. These available formats are listed in the pop-up menu shown in Figure 5.2. On the other hand, if showUserSettingsDialog is
Figure 5.1 The movie export dialog box.
Exporting Movies 119
P :i:~ i:~84 ~i:i:::84
Sire:am Moule to FLC Movie to Hinted Movie M o v i e to: I:mage Sequence Movie to Picture Movie to QulckTIme Movie Sound to-RIFF Sound to System 7 Sound Sound to Wave SoUnd t o jsLoW Figure 5.2 The pop-up menu listing the available export formats.
set but the movieToFileOnlyExport flag is clear, and if fileType is either MovieFi l eType or O, t h e n the pop-up m e n u also includes items that allow the user to save the movie as a movie file or as a self-contained movie file. Figure 5.2 shows the export formats built into Q u i c k T i m e 4.1. Of course, since movie exporters are components, additional exporters can be a d d e d by third parties. So the pop-up m e n u on your m a c h i n e might contain a few m o r e exporters than you see in Figure 5.2.
Converting to Any Available Export Format Listing 5.1 shows a function QTDX_ExportMovieAsAnyTypeFile that configures the movie export flags and then calls ConvertMovieToFi l e to allow the user to export a movie into any available export format. Listing 5.1
Exporting a movie as a user-selected type.
OSErr QTDX_ExportMovieAsAnyTypeFile (Movie theMovie, FSSpec *theFSSpec)
{
FSSpec l ong OSErr
myFSSpec = *theFSSpec; myFlags = OL; myErr = noErr;
myFlags : createMovieFileDeleteCurFile I showUserSettingsDialog I movieFi leSpecValid I movieToFi I eOnlyExport;
120
Chapter 5 In and Out
/ / export the movie into a f i l e myErr = ConvertMovieToFile( theMovie, // NULL, // &myFSSpec, // // OL, FOUR CHARCODE( 'TVOD'), // smSystemScri pt, // NULL, // myFlags, // NULL); // D
the movie to convert all tracks in the movie the output f i l e the output f i l e type the output f i l e creator the script no resource ID to be returned export flags no specific export component
return (myErr) ;
Because the showUserSettingsDialog flag is set, ConvertMovieToFile displays the movie export dialog box shown in Figure 5.1. W h e n the user selects an output file and export format, ConvertMovieToFi l e creates that file (first deleting it if it already exists) and performs the desired conversion. Note that w h e n ConvertMovieToFile completes, it returns information about the newly created file in the myFSSpec parameter. (This behavior appears to be undocumented.) In our sample application, the theFFSpec parameter passed to OTDX_ExportMovieAsAnyTypeFile points to the file system specification of the open movie file. So we make a local copy of that specification in myFSSpec to avoid overwriting the movie's file information.
Converting to a Specific Export Format I mentioned earlier that we can specify a particular movie export component w h e n calling ConvertMovieToFile if we want to export a movie into a specific format. Let's suppose that we want to allow the user to add a hint track to a movie, so that it can be efficiently streamed across a network. The first thing we need to do is find the movie hinter export component. We can use the Component Manager functions FindNextComponent and OpenComponent to find that component and open an instance of it, like this: ComponentDescri pt i on
myCompDesc;
myCompDesc.componentType = MovieExportType; myCompDesc.componentSubType = MovieFileType; myCompDesc.componentManufacturer = FOURCHARCODE('hint'); myCompDesc.componentFlags = O; myCompDesc.componentFlagsMask = O; myExporter = OpenComponent(FindNextComponent(NULL, &myCompDesc)); m
Exporting Movies 12,1
As you can see, we're asking for a movie export c o m p o n e n t that can create movie files with tracks of type 'hint'. We'll pass myExporter as the last parameter w h e n we call ConvertMovieToFi l e. Now, what flags should we pass to ConvertMovieToFil e? We certainly want to pass createMovieFileDeleteCurFile and movieFileSpecValid. And we certainly don't want to pass movieToFileOnlyExport, because we already know what kind of output file we want to create. But what about showUserSett ingsDi a log? Do we want to display the movie export dialog box shown in Figure 5.1? For some purposes we might, but for the m o m e n t let's suppose that we don't want the user to change the output filename or location (which he or she could do if we displayed the movie export dialog box). So we w o n ' t include showUserSettingsDialog in the flags we pass to ConvertMovieToFi le. Now we've got a slight problem. While we don't want the user to change the output filename or location, we might want the user to change the settings used by the movie hinter export component. If the movie export dialog box were displayed, the user could click the Options button to display the settings dialog box shown in Figure 5.3. Luckily, we can use the movie hinter export component directly and ask it to display its settings dialog box, by calling the MovieExportDoUserDialog function like this: MovieExportDoUserDialog(myExporter, theMovie, NULL, O, O, &myCancelled);
Figure 5.3 The movie hinter settings dialog box.
122
Chapter 5 In and Out
So at this point, we've found the appropriate movie hinter export component and displayed its settings dialog box to allow the user to select the desired hinter settings. We can finish up by calling ConvertMovieToFile, specifying the movie hinter export component: myErr = ConvertMovieToFi le(theMovie, NULL, &myHintedFi le, MovieFi leType, FOUR CHAR CODE('TVOD'), smSystemScript, NULL, myFlags, myExporter); m
Is this line of code completes successfully, the specified movie will have been exported into a hinted movie file.
Using Movie Export Settings Imagine now that we've got a large number of movie files to which we want to add hint tracks. Imagine further that we want to use some specific nondeo fault settings for the hinting. It would be tedious to have to open each movie file and configure its export settings in the dialog box displayed by MovieExportDoUserDialog. Instead, it would be better to configure those settings once, save them somewhere, and then instruct the movie hinter export component to use those saved settings. The processing required to make this happen is actually rather simple. Let's suppose that theFSSpecPtr is a pointer to a file system specification for a file that contains a saved copy of the desired exporter settings. Then we can use the function QTDX_GetExporterSettingsFromFi le defined in Listing 5.2 to read those settings and install them as the exporter's active settings. Listing 5.2. Reading exporter settings from a file. OSErr QTDX_GetExporterSetti ngsFromFi I e (Movi eExportComponent theExporter, FSSpecPtr theFSSpecPtr)
{
Handle ComponentResul t
myHandl e = NULL; myErr = fnfErr;
myHandl e = QTDX_ReadHandleFromFi I e(theFSSpecPtr) ; i f (myHandle == NULL) goto bai I ; myErr = MovieExportSetSetti ngsFromAtomContai ner (theExporter, (QTAtomContainer)myHandl e) ; bail 9 i f (myHandle ! = NULL) Di sposeHandl e (myHandle) ; return((OSErr)myErr) ;
Exporting Movies
123
The function QTDX_ReadHandleFromFile reads the data in the specified file into a handle; there is nothing especially interesting about it so we w o n ' t discuss it further. The important function here is MovieExportSetSettingsFromAtomContainer, which takes that handle of data, interprets it as an atom container, and uses the settings in that atom container as the current settings of the specified movie exporter. An atom container is a handle to a block of m e m o r y that is structured in a hierarchical arrangement of container atoms (which contain other atomsl and leaf atoms (which contain data). In a future chapter, we'll take a closer look at the structure of atom containers and see how to create and parse them. But for the moment, we can remain blissfully ignorant of those details, because we can use the function MovieExportGetSettingsAsAtomContainer to create the settings atom container. Listing 5.3 shows our function that saves the current settings of the specified movie exporter into a file.
Listing 5.3 Writing exporter settings into a file. OSErr QTDX_SaveExporterSettingslnFile (MovieExportComponent theExporter, FSSpecPtr theFSSpecPtr)
(
QTAtomContainer ComponentResult
myContainer = NULL; myErr = noErr;
myErr = MovieExportGetSettingsAsAtomContainer(theExporter, &myContainer) ; i f (myErr i= noErr) goto bai I ; myErr
=
QTDX WriteHandleToFile((Handle)myContainer, theFSSpecPtr) ;
bail 9 i f (myContainer ! = NULL) QTDisposeAtomContai ner (myContai ner); return((OSErr) myErr) ;
So what we need to do is display the movie hinter export c o m p o n e n t ' s settings dialog box once, allow the user to configure the hinter settings as desired, and then save those settings into a file by calling OTDX_SaveExporterSettingsInFile. Then, we can use those saved settings for any subsequent export operations. Listing 5.4 shows the complete function QTBX_ExportMovieAsHintedMovie that exports a movie as a hinted movie. Notice that this
124
Chapter 5 In and Out
function takes a Boolean parameter that indicates whether it should display the settings dialog box. Listing 5.4 Exporting a movie as a hinted movie. OSErr QTDX_ExportMovieAsHintedMovie (Movie theMovie, Boolean thePromptUser) ComponentDescri pt i on MovieExportComponent long FSSpec FSSpec Boolean Boolean Stri ngPtr St ri ngPt r ComponentResult
myCompDesc; myExporter = NULL; myFlags; myHi ntedFi I e; myPrefs Fi I e; mylsSelected = false; myIsReplacing = false; myPrompt; myFi I eName; myErr = badComponentType;
myFlags = createMovieFileDeleteCurFile I movieFileSpecVal id; myPrompt = QTUti I s ConvertCToPascal String(kHintedMovieSavePrompt) ; myFi leName = QTUti I s ConvertCToPascal String(kHintedMovieFi leName) ; / / get an output f i l e for the hinted movie QTFrame PutFile(myPrompt, myFileName, &myHintedFile, &mylsSelected, &mylsReplacing); i f (!mylsSelected) { myErr = userCanceledErr; goto bai I ;
}
i f (myIsReplacing) { myErr = FSpDelete(&myHintedFile) ; i f (myErr ! = noErr) goto bai I ;
}
/ / find and open a movie export component that can hint a movie f i l e myCompDesc.componentType = MovieExportType; myCompDesc,componentSubType = MovieFi I eType; myCompDesc.componentManufacturer = FOUR CHAR CODE('hi n t ' ) ; myCompDesc.componentFlags = O; myCompDesc.componentFlagsMask = O; myExporter = OpenComponent(Fi ndNextComponent(NULL, &myCompDesc)); i f (myExporter == NULL) goto bai I ;
Exporting Movies
125
/ / get the preferences f i l e for this application QTDX_GetPrefsFi leSpec(&myPrefsFi le, (void *)&myHintedFile) ; / / read existing movie exporter settings from a f i l e ; i f we aren't going to prompt / / the user for exporter settings, these stored settings will be used; otherwise, / / these stored settings will be used as i n i t i a l values in the settings dialog box QTDX_GetExporterSetti ngsFromFi I e (myExporter, &myPrefsFiI e) ; i f (thePromptUser && QTDX_ComponentHasUI(MovieExportType, myExporter)) { Bool ean myCancel Ied = false; / / display a dialog box to prompt the user for desired movie exporter settings myErr = MovieExportDoUserDialog(myExporter, theMovie, NULL, O, O, &myCancelled); i f (myCancelIed) goto bai I ; / / save the existing settings into our preferences f i l e QTDX_SaveExporterSettingslnFi le(myExporter, &myPrefsFile) ; / / export the movie into a f i l e myErr = ConvertMovieToFile( theMovie, NULL, &myHintedFi I e, MovieFi I eType, FOUR CHARCODE( 'TVOD'), smSystemScri pt, NULL, myFlags, myExporter) ;
// // // // // // // // //
bail/ / close the movie export component i f (myExporter ! = NULL) Cl oseComponent(myExporter) ; free (myPrompt) ; free (myFi I eName); return((OSErr)myErr) ;
126
Chapter5 In and Out
the movie to convert all tracks in the movie the output f i l e the output f i l e type the output f i l e creator the script no resource ID to be returned conversion flags hinter export component
I m p o r t i n g Files As we saw in the previous chapter, QuickTime occasionally imports files for us, without our having to explicitly find a movie importer or call any file conversion function. W h e n we call NewMovieFromFile and specify a file that isn't a QuickTime movie file, NewMovieFromFile looks for a movie importer that can handle the data in that file. If it finds one, it automatically uses that importer to convert the file data into the QuickTime movie format and returns the converted movie to us. So we can import files that are not QuickTime movie files simply by handing them to NewMovieFromFile and letting it work its magic. But there are a few details we need to take care of before we pass a file to NewMovieFromFile. First, if we want to mimic the behavior of QuickTime Player's Import m e n u item, then we need to make sure that the list of files displayed to the user in the file selection dialog box does not include QuickTime movie files. We also need to make sure that that list does include files of type 'TEXT' and 'PICT' (which by default are not listed in the file list displayed w h e n Open is chosen but which are displayed w h e n Import is chosen). Then, if we are not using the StandardGetFilePreview function to display that list of files, we need to determine w h e t h e r a file selected by the user needs to be converted into another file before it can be imported. Let's take these two tasks in order.
Filtering Out Movies The list of files displayed in response to selecting the Import m e n u item should not include any files that are QuickTime movies. If we are using the Navigation Services function NavfietFile, we can accomplish this by passing it a file filter function that accepts all files that QuickTime can open, except files of type kQTFileTypeMovie. Our application framework maintains a list of all files that QuickTime can open, either directly or using a movie importer or a graphics importer. So all our import file filter function needs to do is check to see w h e t h e r a candidate file has a type that's in that list but that isn't kQTFileTypeMovie. The function QTDX_FilterFiles defined in Listing 5.5 does just that.
Listing 5.5 Filtering out movies. PASCAL_RTN Boolean QTDX_FilterFiles (AEDesc *theltem, void *thelnfo, void *theCallBackUD, NavFi I terModes theFi I terMode)
{
#pragma unused(theCalIBackUD, theFilterMode) NavFileOrFolderlnfo *mylnfo = (NavFileOrFolderInfo *)theInfo;
Importing Files 1127
i f (gValidFileTypes == NULL) QTFrame_Bui I dFi I eTypeLi st () ; i f (theltem->descriptorType == typeFSS) { i f (!mylnfo->isFolder) ( OSType myType = mylnfo->fi leAndFolder, fi lelnfo, finderInfo, fdType; long myCount; long mylndex; / / see whether the f i l e type is in the l i s t of f i l e types that our application can / / open, but do not allow movie f i l e s myCount = GetPtrSize((Ptr)gValidFileTypes) / (long)sizeof(OSType) ; for (mylndex = O; mylndex < myCount; mylndex++) i f ((myType == gValidFileTypes[mylndex]) && (myType l= kQTFileTypeMovie)) return(true) ; / / i f we got to here, i t ' s a f i l e we cannot open return(false);
/ / i f we got to here, i t ' s a folder or non-HFS object return(true) ;
If we are running on Windows, where we are using the Standard File Package, it's even simpler. We can just pass StandardGetFilePreview a list of file types that includes all types that QuickTime can open, minus kQTFi l eTypeMovie. When we constructed the list of file types gVal idFi 1eTypes, we put kQTFileTypeMovie in the first position. So we can just pass a pointer that begins at the second file type, like this: myTypeLi stPtr = (QTFrameTypeListPtr)&gVal idFi leTypes [ i ] ; myNumTypes - (short) (GetPtrSize((Ptr)gVal idFileTypes) / sizeof(OSType)) - 1;
You might be wondering w h y we didn't also use a custom file filter function with StandardGetFilePreview. On the Macintosh, this would work fine; but on Windows, only the list of file types is used to determine which files to show in the file-opening dialog box.
128
Chapter5 In and Out
Importing In Place QuickTime can import some types of files without first having to make a copy of the file data. These kinds of files can be imported in place--meaning that the associated movie importer can construct a movie that directly references that data. Other kinds of files cannot be imported in place. For instance, w h e n we select QuickTime Player's Import m e n u item and choose a file of type 'PICT', we are presented with the dialog box shown in Figure 5.4, which asks us to specify a new file to hold the converted picture data. W h e n we specify a new file, the selected 'PICT' file is converted into that file and then the converted file is opened in a movie window. If we are using StandardGetFi lePreview to present a list of openable files to the user, we don't need to know w h e t h e r a file can be imported in place. StandardGetFi l ePreview determines this by itself and presents the file conversion dialog box whenever the selected file cannot be imported in place. But if we are using the Navigation Services programming interfaces, we do need to figure this out. Listing 5.6 defines a function that determines whether a given file can be imported in place. As you can see, we first find a movie importer that can open files whose type is that of the specified file (on the Macintosh) or whose file extension is that of the specified file (on Windows). Then we call GetComponentInfo to get a set of flags that specify the capabilities of that importer. For present purposes, we need to see w h e t h e r the bit canMovieImportInPlace is set.
Figure 5.4 The file conversion dialog box.
Importing Files 1 2 9
Listing 5.6 Determining whether a file can be imported in place. Boolean QTDX_FileCanBelmportedlnPlace (FSSpec *theFSSpec) ComponentDescription Component Boolean OSType unsigned long OSErr #if TARGETOS MAC FInfo m
myCompDesc; myComponent = NULL; myCanlmportlnPlace = false; mySubType; myFlags = O; myErr = noErr;
- _
myFileInfo;
/ / get the f i l e type of the specified f i l e myErr = FSpGetFInfo(theFSSpec, &myFilelnfo) ; i f (myErr l= noErr) goto bai I ; mySubType = myFilelnfo, fdType; #endi f #if TARGETOS WIN32 / / get the filename extension of the specified f i l e myErr = QTGetFileNameExtension(theFSSpec->name, OL, &mySubType); i f (myErr I= noErr) goto bai I ; m
m
myFl ags = movielmportSubTypelsFi leExtension; #endif myCompDesc.componentType = MovielmportType; myCompDesc,componentSubType = mySubType; myCompDesc.componentManufacturer = O; myCompDesc,componentFl ags = myFlags; myCompDesc,componentFl agsMask = myFlags; myComponent = FindNextComponent(NULL, &myCompDesc); i f (myComponent I= NULL) { GetComponentlnfo(myComponent, &myCompDesc, NULL, NULL, NULL); i f (myCompDesc.componentFlags & canMovielmportlnPlace) myCanlmportlnPlace = true;
}
bail: return (myCanlmportInPl ace) ;
}
130
Chapter 5 In and Out
importing Files N o w we have all the pieces we n e e d to handle the I m p o r t m e n u item. First we do the necessary w o r k to limit the files displayed in the file-opening dialog box to any files that Q u i c k T i m e can import but that are not Q u i c k T i m e movie files. T h e n we check to see w h e t h e r the file selected by the user can be i m p o r t e d in place. If it cannot, we n e e d to display a file-saving dialog box to elicit a new file from the user; we also need to convert the selected file into a movie file, w h i c h we can do using the ConvertFileToMovieFile function. Finally, we pass the converted file (or the originally selected file, if it can be imported in place) to our function QTFrame_OpenMovieInWindow, w h i c h in turn calls NewMovieFromFi l e. Listing 5.7 puts this all together.
Listing 5.7 Importing a file. OSErr QTDX_ImportAnyNonMovie (void)
{
QTFrameFileFilterUPP QTFrameTypeLi stPtr short Movie FSSpec FSSpec StringPtr OSErr
myFileFilterUPP = NULL; myTypeListPtr = NULL; myNumTypes = O; myMovie = NULL; myFi I eToConvert; myConvertedFi I e; myPrompt = QTUti I s ConvertCToPascal Stri ng (k ImportSavePrompt) ; myErr = noErr; D
# i f TARGET OS WIN32 myTypeLi stPtr = (QTFrameTypeListPtr)&gVal i dFi I eTypes [I] ; myNumTypes = (short) (GetPtrSize((Ptr)gValidFileTypes) / sizeof(OSType)) - I; #endif / / let the user select an openable f i l e from any f i l e s that aren't movie f i l e s myFi I eFi I terUPP = QTFrame_GetFi I eFi I terUPP ( ( ProcPt r) QTDX_Fi I terF i I es ) ; myErr = QTFrame_GetOneFiI eWi thPrevi ew(myNumTypes, myTypeListPtr, &myFiI eToConvert, (void *)myFi leFi IterUPP) ; i f (myErr l= noErr) goto bai I ; myConvertedFi le = myFileToConvert; / / determine whether the selected f i l e needs to be converted into another f i l e before / / QuickTime can open i t ; i f so, do the conversion # i f TARGET OS MAC i f (!QTDX_FileCanBelmportedlnPl ace(&myFi leToConvert)) { m
Importing Files 131
Boolean Boolean
mylsSelected = false; mylsReplacing = false;
/ / display the p u t - f i l e dialog to save the converted f i l e QTFrame_PutFile(myPrompt, myFileToConvert.name, &myConvertedFile, &myIsSelected, &mylsRepl acing) ; i f (!mylsSel ected) goto bai I ; / / delete any existing f i l e of that name i f (myIsReplacing) { myErr = DeleteMovieFi le(&myConvertedFi le) ; i f (myErr I= noErr) goto bai I ;
}
/ / import the f i l e into a movie myErr = ConvertFileToMovieFile( &myFi I eToConvert, &myConvertedFi I e, FOUR CHAR CODE('TVOD'), smSystemScri pt, NULL, OL, NULL, gMovi eProgressProcUPP, OL); m
}
m
// // // //
the the the the
f i l e to convert f i l e to convert i t into output f i l e creator script
#endif / / now open the (possibly) converted f i l e in a window i f (myErr == noErr) QTFrame_OpenMovielnWi ndow(NULL, &myConvertedFiI e) ; bail 9 i f (myFileFilterUPP ! = NULL) Di sposeRouti neDescri ptor (myFi I eFi I terUPP) ; free (myPrompt) ; return (myErr) ;
It's important to understand that our Macintosh implementation of the Import menu item lacks a key feature provided automatically by Standard-
132
Chapter5 In and Out
6etFilePreview. To wit: the file-saving dialog box displayed by the call to QTFrame_PutFi]e in Listing 5.7 does not contain an Options button that allows the user to modify any settings supported by the movie importer capable of opening the selected file. [Look again at Figure 5.4 to see the Options button provided by StandardGetFilePreview.) To add a custom button to the standard Navigation Services file-saving dialog box would take us too far afield right now. Let's put this item on our list of features to add at some time in the future.
Default Progress Functions Unless a movie is very short, the calculations and disk accesses involved in {for instance) exporting it to a new format can take several minutes, if not considerably longer, even on today's relatively fast machines. From its very inception, QuickTime has provided a way to inform the user that a lengthy operation is in progress and to provide some indication of how much of the operation has completed. These tasks are accomplished using a movie progress function, which typically displays a movie progress dialog box. In this section and the following two sections, we'll see how to display and manage a movie progress dialog box. By far the easiest way to display a progress dialog box is to use QuickTime's default progress function, which displays and manages a dialog box like the one shown in Figure 5.5 (on Macintosh computers) or in Figure 5.6 (on Windows computers). As you can see, these dialog boxes contain a progress bar control that shows the relative amount of completion of the operation, a text string indicating the operation that's in progress, and a button to
Figure 5.5 The default progress dialog box (Macintosh).
Figure 5.6 The default progress dialog box (Windows).
Default Progress Functions 133
cancel the operation. There are a dozen or so operations that can trigger the display of a movie progress dialog box, including exporting and importing movies, cutting or pasting movie segments, loading a movie into memory, and saving a movie as a self-contained movie. We can instruct QuickTime to display this default progress dialog box during lengthy operations on a particular movie by calling the SetMovieProgressProc function and p a s s i n g - 1 as the progress function universal procedure pointer. Our basic application framework includes this line of code to set the default function for each movie we open:
SetMovieProgressProc(myMovie,
(MovieProgressUPP)-l, O) ;
We need to take the word "default" here with a grain of salt, however. We'll get this default progress dialog box only if we call SetMovi eProgressProc with -1 as the second parameter. If we open a movie, fail to call SetMovieProgressProc, and then initiate a lengthy operation, QuickTime does not display any progress dialog box at all. This might lead the user to think that our application has frozen, so it's almost always a good idea to use a progress function that provides some visual feedback.
Custom Progress Functions The default progress function in fact does a fair amount of work for us. It displays the default progress dialog box, continually updates the progress bar control in the dialog box, and responds to user clicks on the Stop button. In addition, it looks for user presses on the Escape key and Command-Period key combination and interprets those presses as equivalent to a click on the Stop button. Finally, it displays a message indicating which operation is in progress. Not bad for a single line of code. Still, it's tempting to want to jazz things up a bit, and QuickTime provides a way for us to install a custom progress function that is called periodically during a lengthy operation. We're free to do just about anything in our custom progress function. We can display a different dialog box or message, replace the progress bar control by some other progress indicator, play sounds, and the like. For the moment, we'll restrain our urges to bloat our progress dialog box with features. Instead, we'll retain the basic appearance and operation of the default progress dialog box, while adding two features: we'll add a text message that indicates the approximate amount of time remaining in the operation, and we'll add a picture that is gradually erased (from bottom to top) as the operation progresses. Figure 5.7 shows our custom progress dialog box in action. As you've already seen, we install a custom progress function by passing its universal procedure pointer to the SetMovieProgressProc. So, if our cus-
134
Chapter 5 In and Out
Figure 5.7 The custom progress dialog box of QTDataEx. tom progress function is QTDX_MovieProgressProc, we can install it by executing these lines of code" gMovieProgressProcUPP = NewMovieProgressProc (QTDX_MovieProgressProc) ; i f ((**theWindowObject).fMovie ! = NULL) SetMovi eProgressProc ( (**theWi ndowObject). fMovi e, gMovieProgressProcUPP, (long) theWi ndowObject) ;
The third p a r a m e t e r to SetMovieProgressProc is an arbitrary 32-bit reference constant that's passed to the movie progress function each time it's called. Here we are passing the w i n d o w object associated with the movie. We don't actually use that value in our custom progress function, but it's not hard to imagine things we could do with that information. Our custom movie progress function has this declaration: PASCAL_RTN OSErr QTDX_MovieProgressProc (Movie theMovie, short theMessage, short theOperation, Fixed thePercentDone, long theRefcon);
As you can see, this f u n c t i o n has five p a r a m e t e r s , two of w h i c h are the movie being o p e r a t e d u p o n and the r e f e r e n c e c o n s t a n t that we specified w h e n we called SetMovieProgressProc. The theMessage p a r a m e t e r is a value that indicates w h y our progress function is being called. The Movie Toolbox defines these constants for this parameter: enum { movi eProgressOpen movi eProgressUpdatePercent movi eProgressCl ose
};
=0, =
1~
=2
Custom Progress Functions 135
So our custom progress function gets called w h e n the lengthy operation is started, w h e n it has stopped, and at various percentages of completion. The progress function is called often enough for us to keep our dialog box updated in a fairly smooth manner. If theMessage is movieProgressUpdatePercent, then the thePercentDone parameter indicates the percentage of completion. This percentage is always specified as a Fixed value between 0.0 and 1.0. The parameter the0peration is a constant that specifies which operation has triggered the custom progress function. The Movie Toolbox defines these 12 constants: enum { progressOpFl atten prog res sOpInsert Trac kSegment progressOplnsertMovi eSegment progressOpPaste progressOpAddMovieSel ecti on progressOpCopy progressOpCut prog res sOpLoadMovi eI ntoRam prog res sOpLoadTrack I ntoRam progres sOpLoadMedi aI ntoRam progres sOpImportMovi e progressOpExportMovi e
};
=1, =2, = 3~ = 4,
= 5~ = 6~ = 7~ = 8p = 9t = 10, = 11, = 12
We'll use this information to determine which string to display at the top of our custom progress dialog box, like this" GetDialogltem(myDialog, kProgressTextltemID, &myltemKind, &myltemHandle, &myI temRect) ; i f ((theOperation > O) && (theOperation <= progressOpExportMovie)) { GetlndString(myString, kOperationsStringsResID, theOperation); SetDialogltemText(myltemHandle, myString) ;
}
Our application's resource file contains a 'STR#' resource of ID k0perationsStringsResID that contains strings describing each of the 12 possible operations our movie progress function can be called to handle. We'll use the same strings that are used in the default progress dialog box. (In fact, I simply "borrowed" the 'STR#' resource of ID -19183 from the QuickTime extension.) Our custom progress function is in fact fairly simple in structure, if a tad long. It simply inspects the theMessage parameter to see what phase of the operation is occurring and then switches to the appropriate code to handle
136
Chapter5 In and Out
that phase. W h e n our function receives the movieProgressOpen message, it opens our custom progress dialog box and performs any initial configuration of the dialog box. W h e n it receives the movi eProgressUpdatePercent message, it updates the progress bar control and erases the appropriate section of the picture in the dialog box. During this phase, our progress function also looks to see if the user wants to cancel the operation. Finally, w h e n it receives the movieProgressClose message, it disposes of the dialog box and cleans up. Let's consider each of these phases in a bit more detail.
Opening the Dialog Box When our custom progress function receives the movi eProgressOpen message, we'll open a dialog box in the standard way, by reading it from our resource file: myDialog = GetNewDialog(kProgressDialogResID, NULL, (WindowPtr)-l) ;
If we are successful in opening the dialog box (that is, if myDi a log is nonNULL), then we need to display the appropriate operation message (which we just saw how to do) and configure the progress bar control. To configure the control, we want to set its m i n i m u m value to 0 and its m a x i m u m value to some arbitrary value, defined by the constant kProgressBarMaxVal ue: GetDialogltem(myDialog, kProgressBarltemID, &myltemKind, &myltemHandle, &myI temRect) ; myBar = (ControIHandle)myltemHandle; SetControIMinimum(myBar, O) ; SetControIMaximum(myBar, (SInt16) kProgressBarMaxVal ue) ;
In fact, we could probably dispense with calling SetControlMinimum and SetContr01Maximum here, because the m i n i m u m and m a x i m u m values are also set in the resource file; but it d o e s n ' t hurt to make sure that those values are what we expect them to be. W h e n it comes time to update the progress bar control, it will be important that the control m a x i m u m be set correctly. Note that we are saving the control handle of the progress bar control in the local static variable myBar. Next, we need to set a drawing procedure for the user item that contains the progress picture. We can do so like this: GetDialogltem(myDialog, kProgressPictureltemlD, &myltemKind, &myltemHandle, &myI temRect) ; SetDialogltem(myDialog, kProgressPictureltemID, myltemKind, (Handl e) gProgressUser I temProcUPP, &myI temRect) ;
Custom Progress Functions 137
The global variable gProgressUserltemProcUPP is a universal procedure pointer to our function QTDX_ProgressBoxUserItemProcedure, which reads the picture from a 'PICT' resource in our application's resource file and draws it in the rectangle enclosing the user item, using the QuickDraw function DrawPicture. Now we are ready to display our custom progress dialog box to the user. We'll call MacShowWindow and DrawDialog to do so. We shall also call the useritem drawing procedure directly, since that results in a noticeably quicker update of the user item when the box is first drawn. MacShowWindow(myDial og) ; QTDX_ProgressBoxUserltemProcedure(myDial og, kProgressPi cturel temID) ; DrawDial og (myDial og) ;
Finally, we'll call the TickCount function to get the current time, so that we can later display an estimate of the remaining time: myTicks = TickCount();
Handling Progress Messages When our custom progress function receives a movieProgressUpdatePercent message, the thePercentDone parameter indicates the percentage of completion. As mentioned earlier, the value passed in the thePercentDone parameter is always between 0 and fixed1 (that is, between OxO0000000 and OxO0010000). Just to be safe, however, we'll first verify that the value passed to our custom progress function lies within the expected range: i f ((thePercentDone < O) [[ (thePercentDone > fixed1)) break;
The next thing we want to do is update the progress bar control. Remember that the acceptable values for that control lie between 0 and kProgressBarMaxValue, so we need to scale thePercentOone to lie within that range. We can do this with the following lines of code: i f (myBar I: NULL) { SetControIValue(myBar, (SInt16)Fix2Long(FixMul (thePercentDone, Long2Fi x (kProgressBarMaxVal ue) ) ) ) ;
}
We also want to erase the appropriate bottom portion of the picture in the custom dialog box. Once again, this mainly involves scaling the height of the picture rectangle by the percentage we are passed, as follows:
138
Chapter5 In and Out
GetDialogltem(myDialog, kProgressPictureltemID, &myltemKind, &myltemHandle, &myI temRect) ; MacSetRect (&myEraseRect, myItemRect, l e f t , myltemRect.bottom - (SInt16)Fix2Long(FixMul (thePercentDone, Long2Fi x (myI temRect, bottom - myI temRect, top) ) ), myl temRect, right, myl temRect, bottom) ; EraseRect (&myEraseRect) ;
The final visible thing we want to do is print out an estimated time remaining in the operation. In OTDX_MovieProgressProc, we retrieve the rectangle surrounding the area where we want to draw the remaining time estimate and then call the function OTDX_EstimateRemainingTime, like this: GetDialogltem(myDialog, kProgressTimeltemID, &myltemKind, &myltemHandle, &myI temRect) ; QTDX_EstimateRemainingTime(&myltemRect, thePercentDone, TickCount() - myTicks); QTDX_EstimateRemaini ngTime is defined in Listing 5.8.
Listing 5.8 Displaying the estimated time remaining. void QTDX_EstimateRemainingTime (Rect *theRect, Fixed thePercentDone, UInt32 theTi cksEl apsed) char Fixed UInt32 Rect StringPtr
myStri ng [321 ; myEstTicks; myRemSeconds; myEraseRect; myPStri ng = NULL;
myEstTicks = FixMul (FixDiv(fixedl, thePercentDone), Long2Fix(theTicksElapsed)) ; myRemSeconds = Fix2Long(FixDiv(myEstTicks - Long2Fix(theTicksElapsed), Long2Fix(60))) ; TextSi ze (kTimeRemaini ngLabel Si ze) ; TextFont (1) ; myPString = QTUtiIs ConvertCToPascalString(kTimeRemainingLabel) ;
MoveTo(theRect->left, theRect->bottom) ; DrawStri ng (myPStri ng) ;
Custom Progress Functions 139
MacSetRect (&myEraseRect, theRect->left + StringWidth(myPString), theRect->top, theRect->right, theRect->bottom) ; free(myPStri ng) ; EraseRect (&myEraseRect) ; MoveTo(myEraseRect.left, myEraseRect.bottom) ; / / the early percentages give inaccurate estimates, so don't start displaying the / / time until we've reached a minimum threshold i f (thePercentDone < kMinimumUsefuIPercent) return; i f (myRemSeconds == I) sprintf(myString, "~ou second", myRemSeconds); else sprintf(myString, "~ou seconds", myRemSeconds); myPString = QTUtils ConvertCToPascaIString(myString) ; DrawString (myPStri ng) ; free(myPStri ng) ;
Handling User Actions One other thing we need to do in response to the movieProgressUpdatePercent message is to check to see w h e t h e r the user has performed any actions that affect any items in our custom progress dialog box. In our case, that means that we have to check to see w h e t h e r the user has clicked the Stop button or pressed an equivalent key or key combination. This turns out to be a bit tricky. Normally, w h e n we are displaying a modal dialog box, we can sit in a loop calling the Modal Di a log function, waiting for the user to hit the Stop button (or perform some other operation that our modal dialog event filter function will map into a click on the Stop button). But we cannot adopt that strategy here, because our custom progress function is being called periodically by the Movie Toolbox, which is busy performing some lengthy operation. In other words, w e ' r e not in charge w h e n we're inside of our progress function; rather, the Movie Toolbox is allotting us some time periodically to update our progress dialog box. Since we cannot sit around waiting for the user to perform some relevant action, the next best thing we can do is go looking for events that interest us
140
Chapter 5 In and Out
w h e n e v e r we receive a movieProgressUpdatePercent message. For instance, we can look for clicks on the Stop button by searching the event queue for mouse-down events, like this: GetDi al ogl tem(myDial og, kProgressStopButtonl temID, &myltemKi nd, &myltemHandle, &myltemRect) ; i f (WaitNextEvent(mDownMask, &myEvent, O, NULL)) { GlobaIToLocal (&myEvent.where) ; i f (TrackControl ((ControlHandle)myItemHandle, myEvent.where, NULL)) myErr = userCanceledErr;
}
And we can look for key presses by searching the event q u e u e for keyDown events, like this: i f (WaitNextEvent(keyDownMask, &myEvent, O, NULL)) { myKey = myEvent.message & charCodeMask; i f (myEvent.modi fiers & cmdKey) i f (IsCmdChar(&myEvent, kPeriod)) myKey = kEscapeKey; i f (myKey == kEscapeKey) { uns i gned l ong myTi cks; / / simulate a click on the Stop button Hi I iteControl ((ControIHandle)myltemHandle, kControl ButtonPart) ; Delay(kMyButtonDelay, &myTicks) ; HiliteControl ((ControIHandle)myltemHandle, false) ; myErr = userCanceledErr;
Notice that w h e n we find a click on the Stop button or the appropriate key press, we set our function return value to userCanceledErr. W h e n e v e r our custom progress function returns any nonzero value, the Movie Toolbox cancels the operation in progress. Now, this all works fairly well for handling user actions. Our progress function is called often enough that it can pick up these user actions and respond to them just about as quickly as if it were continually calling ModalDialog inside a loop. There is only one remaining complication: if we run our application with this code for our custom progress function on Windows, the user's clicks on the Stop button will not be found w h e n we call WaitNextEvent. {This is due to some fairly low-level details of the QuickTime
Custom Progress Functions '1141
Media Layer, which need not concern us here.) The Escape key presses will be found just fine, and all the item-updating code works just fine. It's just that clicks on the Stop button are not to be found in the event queue. The solution to this problem is to install a callback procedure that is executed whenever an event occurs for one of the active control items in the custom dialog box. Our callback function can be very simple: all it needs to do is look for clicks on the Stop button and then set a flag to pass that information back to our custom progress function. Listing 5.9 shows our dialog callback function. Listing 5.9 Looking for clicks on the Stop button on Windows. # i f TARGET OS WIN32 static void QTDX_ModelessCallback (EventRecord *theEvent, DialogPtr theDialog, short theltemHit)
{
#pragma unused(theEvent, theDialog)
}
i f (theltemHi t == kProgressStopButtonltemID) gUserCancelled = true;
#endif
Now we need to do two other things. In the code handling the movieProgressOpen message, we need to install this callback procedure and initialize the global flag, like this: # i f TARGET OS WIN32 SetModel essDi aI ogCaI I backProc (myDial og, (QTModelessCal I backUPP)QTDX_ModelessCal I back) ; gUserCancel I ed = false; #endi f
And then in our code that handles the movieProgressUpdatePercent message, we need to see w h e t h e r the global flag has been set by the callback procedure; if so, we set the function return value to some nonzero value and then jump to the end of the routine. # i f TARGETOS WIN32 i f (gUserCancelled) { myErr = userCancel edErr; goto bai I ; m
}
#endi f
142
Chapter 5 In and Out
If you'd prefer to avoid using global variables here, you could instead pass information to and from the callback procedure by setting the w i n d o w reference constant of the custom progress dialog box, using the SetWRefCon function.
Closing the Dialog Box W h e n the Movie Toolbox is finished with the operation that triggered our progress function or w h e n it receives a nonzero return value from our progress function, it sends our progress function a moviePr-ogressClose message. This is our signal to remove our custom progress dialog box from the screen and do any other necessary cleanup. Our custom progress function executes these few lines to dispose of our dialog box and reset the static variables: case movieProgressCl ose: i f (myDialog l= NULL) Di sposeDi al og (myDial og) ; myDial og = NULL; myBar = NULL; break;
So now we've handled all three messages. Our complete progress function is shown in Listing 5.10. Listing 5.1t:) Displayinga custom movie progress dialog box. PASCAL_RTN OSErr QTDX_MovieProgressProc (Movie theMovie, short theMessage, short theOperation, Fixed thePercentDone, long theRefcon)
{
#pragma unused(theMovie, theRefcon) CGrafPtr GDHandle static DialogPtr static ControIHandle static UInt32 short Handle Rect Rect Str255 EventRecord char OSErr
mySavedPort = NULL; mySavedDevice = NULL; myDialog = NULL; myBar = NULL; myTicks = O; myltemKi nd; myltemHandl e = NULL; myI temRect; myEraseRect; myString; myEvent; myKey; myErr = noErr;
Custom Progress Functions 143
GetGWorl d(&mySavedPort, &mySavedDevice) ; i f (myDialog ! = NULL) SetGWorld((CGrafPtr)myDialog, GetMainDevice()) ; switch (theMessage) { case movieProgressOpen: / / display the progress dialog box myDialog = GetNewDialog(kProgressDialogResID, NULL, (WindowPtr)-l) ; i f (myDialog ! = NULL) { / / set the dialog box as the current graphics port SetGWorld((CGrafPtr)myDialog, GetMainDevice()) ; SetDi al ogCancel Item (myDial og, kProgressStopButton I temID) ; / / configure the progress bar control GetDi al ogl tem (myDial og, kProgressBar I temID, &myltemKind, &myltemHandle, &myltemRect); myBar = (ControIHandle)myltemHandle; SetControIMinimum(myBar, O) ; SetControIMaximum(myBar, (SInt16) kProgressBarMaxVal ue) ; / / set the dialog box text that describes the current operation GetDi al ogltem(myDi al og, kProgressTextltemID, &myltemKind, &myltemHandle, &myltemRect); i f ((theOperation > O) && (theOperation <= progressOpExportMovie)) { GetlndString(myString, kOperationsStringsResID, theOperation); SetDialogltemText(myItemHandle, myString) ;
}
/ / set a user-item drawing procedure for the picture rectangle GetDi al og Item(myDi al og, kProgressPi ctureltemID, &myltemKind, &myltemHandle, &myltemRect) ; SetDi al ogl tem (myDial og, kProg res sPi cture I teml D, myl temKi nd, (Handl e) gProgres sUser I temProcUPP, &myI temRect) ; / / show the dialog box and draw the picture in the user item rectangle MacShowWindow(myDial og) ; QTDX_ProgressBoxUserI temProcedure (myDiaI og, kProgressPicture I teml D) ; DrawDial og (myDial og) ;
144
Chapter 5 In and Out
myTicks = TickCount(); # i f TARGETOS WIN32 / / set a dialog callback procedure, to notify our progress proc / / that the user has cancelled SetModel essDi al ogCal I backProc (myDial og, (QTModelessCal I backUPP)QTDX_ModelessCaI I back) ; / / i n i t i a l i z e the variable that keeps track of whether the user has cancelled gUserCancel I ed = false; #endif break; case movieProgressUpdatePercent 9 # i f TARGETOS WIN32 i f (gUserCancelled) { myErr = userCanceledErr; goto bai I ;
/ / stop the operation
}
#endif / / check to see whether the user wants to cancel the operation / / get the rectangle surrounding the Stop button GetDi aI ogl tem (myDial og, kProgressStopButtonI temID, &myltemKind, &myltemHandle, &myltemRect) ; / / check for user clicks on the Stop button i f (WaitNextEvent(mDownMask, &myEvent, O, NULL)) { GlobaIToLocal (&myEvent.where) ; i f (TrackControl ((ControIHandle)myltemHandle, myEvent.where, NULL)) myErr = userCanceledErr; / / stop the operation
}
/ / check for user presses on the Escape key or on equivalent key combinations i f (WaitNextEvent(keyDownMask, &myEvent, O, NULL)) { myKey = myEvent.message & charCodeMask; i f (myEvent.modi fiers & cmdKey) i f (IsCmdChar(&myEvent, kPeriod)) myKey = kEscapeKey; i f (myKey : : kEscapeKey) { unsigned l o n g myTicks;
Custom Progress Functions 145
/ / simulate a click on the Stop button Hi I iteControl ((ControIHandle)myItemHandle, kControl ButtonPart) ; Delay(kMyButtonDelay, &myTicks) ; HiliteControl ((ControIHandle)myItemHandle, false) ; myErr = userCanceledErr;
/ / stop the operation
/ / update our progress dialog box i f (myBar != NULL) { / / thePercentDone is in the range 0 to fixed1 (OxO0000000 to OxO0010000); / / we need to scale i t to l i e within the range 0 to kProgressBarMaxValue SetControIValue(myBar, (SInt16)Fix2Long(FixMul (thePercentDone, Long2Fi x (kProgressBarMaxVal ue) ) ) ) ;
}
/ / erase the appropriate bottom portion of the picture GetDi al ogltem(myDi al og, kProgressPi ctureltemID, &myltemKind, &myltemHandle, &myItemRect); MacSetRect ( &myEraseRect, myI temRect. I eft, myI temRect.bottom (SInt16) Fi x2Long(Fi xMul (thePercentDone, Long2Fi x (myl temRect .bottom - myI temRect, top) ) ), myltemRect, right, myl temRect, bottom) ; EraseRect (&myEraseRect) ; / / update the estimated time remaining GetDialogltem(myDi alog, kProgressTimeltemID, &myltemKind, &myltemHandle, &myltemRect); QTDX_EstimateRemainingTime(&myltemRect, thePercentDone, TickCount() - myTicks) ; break; case movieProgressCl ose: / / remove our progress dialog box i f (myDialog ! = NULL) Di sposeDi aIog (myDiaIog) ; myDial og = NULL; myBar = NULL; myTicks = O;
146
Chapter 5 In and Out
}
break;
bail 9 SetGWorld(mySavedPort, mySavedDevice); return (myErr) ;
Progress Functions for Image Operations So far, we've focussed on implementing a custom progress function for operations involving movies (importing, exporting, cutting, pasting, and so forth). But operations on images can also take a significant a m o u n t of time. For instance, compressing or decompressing an image can sometimes take quite a while, especially if the compression or decompression algorithm is processor-intensive and the image is fairly large. So w e ' d like to be able to provide the same sort of progress notification to the user that we can now provide with lengthy operations on movies. We can do this by defining an
image progress function. In general, everything we've learned so far about movie progress functions will be of use in writing an image progress function. The declaration of an image progress function is slightly different than that of a movie progress function. Here's how we might declare an image progress function: PASCAL_RTN OSErr QTDX_ImageProgressProc (short theMessage, Fixed thePercentDone, long theRefCon);
As you can see, there is no parameter specifying the movie (since, of course, there is no movie being operated upon here). Also, there is no indication of which operation is triggering our image progress function. The main difference between an image progress function and a movie progress function is in the way we install the progress function. As we've seen, we can call SetMovieProgressProc to install a movie progress function for lengthy operations involving a movie. W h e n we call SetMovieProgressProc, we need to pass it a universal procedure pointer to the movie progress function. When installing an image progress function, however, we instead need to pass the address of a structure of type I CMProgressProcRecord, which is defined (in ImageCompression.h) like this: struct ICMProgressProcRecord { ICMProgressUPP progressProc; long progressRefCon;
};
Progress Functions for Image Operations 147
Using the ICMProgressProcRecord record, we specify both the image progress function and the reference constant. For example, we could call GraphicsImportSetProgressProc to install an image progress function for any image handled by our application like this: i f ((**theWindowObject).fGraphicslmporter ! = NULL) { I CMProgressProcRecord myProcRec; myProcRec,progressProc = glmageProgressProcUPP; myProcRec,progressRefCon = (l ong) theWi ndowObject; Graphi cs ImportSet Progress Proc ( (**t heWindowObject). fGraphi cs Importer, &myProcRec) ;
At this point, whenever the graphics importer associated with the image undertakes some lengthy operation, it will call our image progress function OTDX_ImageProgressProc. (To get the default image progress function, pass (ICMProgressProcRecordPtr)-I as the second parameter, in place of &myProcRec.) We could of course display a custom progress dialog box devoted especially to images and manage that dialog box with a new image progress function; for simplicity, however, we'll just call our existing movie progress function, as shown in Listing 5.11.
Listing 5.11 Displayinga custom image progress dialog box. PASCAL_RTN OSErr QTDX_ImageProgressProc (short theMessage, Fixed thePercentDone, 1ong theRefCon)
{ }
return(QTDX_MovieProgressProc(NULL, theMessage, O, thePercentDone, theRefCon));
Notice that we pass NULL for the movie parameter (which is OK since we never use that parameter in the custom movie progress function}. We also pass 0 as the operation parameter. As I mentioned earlier, our custom image progress function does not get notified about which operation is taking a long time; accordingly, we just pass 0 to our movie progress function as the operation parameter. Since 0 is not a valid index in a 'STR#' resource, we won't be able to display a custom message telling the user which operation is in progress. For images, therefore, the custom progress dialog box will contain whatever message is specified in the dialog item resource {which in our case is "Making Progress").
148
Chapter 5 In and Out
T h e Code The code folder accompanying this chapter contains the project files, source code, and resource data of a sample application called QTDataEx, for both Macintosh and Windows. QTDataEx is just like our basic QTShell application, except that the Test menu has been renamed "Movie" and includes three items that illustrate basic uses of movie data exchange components. Figure 5.8 shows the Movie menu of QTDataEx w h e n a QuickTime movie is in the frontmost window.
Figure 5.8
The Movie menu in QTDataEx.
Choosing one of these items results in our application calling one of the functions we've defined earlier (that is, either QTDX_ImportAnyNonMovie, QTDX_ ExportMovieAsAnyTypeFile, or QTDX_ExportMovieAsHintedMovie). Further, the function QTApp_SetupWindowObject installs our custom movie and image progress functions so that any lengthy operations involving movies or images opened by our application will result in the display of our custom progress dialog box. For import operations, where there is no movie initially available, we need to specify the custom progress function as a parameter to the ConvertFileToMovieFile function (see Listing 5.7).
Conclusion In this chapter we've taken a preliminary look at using movie importers and exporters. We've used the high-level functions ConvertFileToMovieFile and ConvertMovieToFile to perform some basic importing and exporting. We've also worked directly with movie exporters when displaying an exporter's settings dialog box (with MovieExportDoUserDi al og) and managing the settings in that dialog box (with MovieExportGetSettingsAsAtomContainer and MovieExportSetSettingsFromAtomContainer). And we've seen how to display and manage the default and custom progress dialog boxes for both movies and images. QuickTime provides a wide array of lower-level importing and exporting capabilities. If we wanted to export only part of a movie, for instance, we could use the MovieExportToFile function. Or, if we wanted to export a movie into memory (and not into a file), we could use the MovieExportToHandle function. As usual, we've barely scratched the surface with movie importers and exporters.
Conclusion
1149
This Page Intentionally Left Blank
Doug's 1st Movie Creating QuickTime Movie Files
Introduction So far in this book, we've learned how to open movie files and allow the user to interact with the movies in those files. We've learned how to perform some basic operations on movies, such as editing them and exporting them under new formats. We've also learned how to open and display image files and to perform some simple transformations on those images. And, for good measure, we've managed to do everything in a fully cross-platform manner so that our applications run both under the Macintosh operating system and under Windows, with exactly the same features. Now it's time to take an important step forward in our journey through QuickTime: it's time to learn how to create QuickTime movie files. Even though QuickTime supports a vast array of types of media (audio, video, text, sprite animation, vector image, virtual reality, and so forth), it expects that all of the media data and the information describing that data will be stored in a specific format, called the Q uickTime movie lile format. This means that once we learn the basic sequence of operations involved in creating files that adhere to this format, we can fairly easily apply that knowledge to create files containing any of these types of media data. For the moment, we're going to restrict our attention to creating a QuickTime movie file with a single video track. But the techniques we learn here will come into play over and over again in the future when we want to create movie files that contain sound data, text, music, sprites, and other types of media data. Happily, the Movie Toolbox provides a set of high-level programming interfaces that we can use to create movie files without having to know very much about the actual details of the QuickTime movie file format. We need to know some of those details, however, so we'll begin by surveying the structure of QuickTime movies and QuickTime movie files. Then we'll be ready to create movie files that exhibit that structure.
151
1"he Structure of Quick'rime M o v i e s What exactly is a QuickTime movie? There are really two answers to this question. So far in this book, we've been concerned primarily with QuickTime movies as they exist in memory. That is to say, we've been working with items of type Movie that we can play, edit, attach movie controllers to, and so forth. We typically obtain these movies by opening Quick Time movie files, which exist on disk (or some other storage medium). The movie file contains the audio and video data that is presented to the user w h e n a movie is being played, as well as other information (called metadata) that describes how that audio and video data is organized and synchronized. Of course, QuickTime isn't limited to playing audio and video data, so we'll need a more general term to refer to the movie data contained in the movie file; let's call it media data. As a rough preliminary characterization, then, we can say that a QuickTime movie file is a file that contains the movie's media data and the associated metadata. (As we'll see later, a movie file might not actually contain the media data, but only references to the media data. For the moment, however, we'll ignore this possibility.) A QuickTime movie, on the other hand, is a structure that contains information about the media data. In effect, a QuickTime movie is just a bookkeeping device that contains the information necessary to retrieve the media data from the QuickTime movie file and present it to the user at the appropriate moment. The exact structure of QuickTime movies is private. If you take a look into the header file Movies.h, you'll see that a variable of type Movie is a pointer to a MovieRecord structure, which is defined like this: struct MovieRecord { long
};
data[l] ;
All we can tell about the structure of a MovieRecord is that it contains at least 4 bytes of data. We can operate on movies only by using the application programming interfaces provided by the Movie Toolbox. A movie consists of one or more tracks, plus some optional movie user data. A track has a starting time and duration and is associated with exactly one kind of media data. So, for instance, a track might hold audio data or video data, but not both. A movie can contain several tracks, each associated with a different kind of media data. Also, a movie can contain several tracks that are associated with the same kind of media data. For instance, a movie might have a video track and several audio tracks (perhaps in different languages).
152
Chapter6 Doug's 1st Movie
The structure of a track is also private. A variable of type Track is a pointer to a TrackRecord structure, which is defined like this: struct TrackRecord { long
};
data[l] ;
A track contains a single media structure (or, more briefly, a media), w h i c h references, but does not contain, the media data associated with the track. Once again, as you've probably guessed, the structure of a media is private. A variable of type Media is a pointer to a MediaRecord structure, w h i c h is defined like this: struct MediaRecord { long
};
data[I];
Figure 6.1 shows a standard way of depicting the structure of a QuickTime movie. As you can see, this movie contains three tracks: two video tracks and one sound track. Associated with a movie is a movie time coordinate system, which provides a means to measure time in a movie. The basic unit of time m e a s u r e m e n t for a movie is the movie's time unit, and the number of time units that elapse per second is the movie's time scale. In the movie shown in Figure 6.1, the time scale is 600, so the time unit is 1/600th of a second. The sound track is scheduled to begin playing after 1 second has elapsed in the movie, and video track 2 is scheduled to begin playing after about 2 seconds have elapsed. QuickTime requires that all tracks start at time 0, but the track data can begin later in the movie; the e m p t y space between the beginning of the movie and the beginning of the track data is called the track offset.
Figure 6.1
The structure of a QuickTime movie.
The Structure of QuickTime Movies
1153
Keep in mind that Figure 6.1 is intended to illustrate the logical structure of a QuickTime movie, not the actual arrangement of bytes in memory. To repeat, the actual structure of a QuickTime movie in m e m o r y is private. The only way to operate on movies, tracks, or media is to use the APIs provided by the Movie Toolbox.
T h e Structure of Q u i c k T i m e M o v i e Files In contrast to the undocumented, private structure of QuickTime movies, the structure of QuickTime movie files is completely public. This means, among other things, that QuickTime movie files can be built on any operating system, whether or not the QuickTime software is available on that operating system. That's because a QuickTime movie file is just a sequence of bytes structured according to an openly documented specification. Note, however, that you don't actually have to know the details of that specification to build QuickTime movie files. The Movie Toolbox provides a set of high-level functions that we can use to build QuickTime movie files with a minimum of fuss. (Whew!) Nevertheless, it will be useful for us to become acquainted with at least some of those details, if only to understand better what it is we are building and why we need to do certain things when building QuickTime movie files. So let's take a quick look at the structure of QuickTime movie files. It turns out that the basic structure of QuickTime movie files has evolved in the years since QuickTime was introduced. Files created back then will still play OK nowadays {at least on Macintosh computers}, but we can make life easier for ourselves and our viewers if we build files that conform to the currently preferred format, which is single-fork, self-contained, Fast Start, interleaved movie files. Let's unravel that mouthful.
Double-Fork and Single-Fork Movie Flies When QuickTime was first released, back in 1991, it ran only on the Macintosh operating system. Accordingly, the default behavior of the Movie Toolbox was to build movie files that took advantage of the dual-fork nature of files in the Macintosh file system, where each file has both a resource fork and a data fork. The movie's metadata was stored in the resource fork and the movie's media data was stored in the data fork. This clean separation of metadata from media data meant that the Movie Toolbox could very easily find the metadata, parse it, and load it into m e m o r y as a QuickTime movie, without ever touching the media data. Figure 6.2 shows the original structure of QuickTime movie files as double-fork files. Figure 6.2 also reveals a bit more about the internal structure of the data and resource forks of a double-fork QuickTime movie file. Consider first the
154
Chapter6 Doug's 1st Movie
Figure 6.2
A double-fork movie file.
data fork. The media data is contained in a structure called an atom. An atom is simply a collection of data preceded by an atom header, w h i c h consists of a 4-byte length value and a 4-byte type. (The length value includes the 8 bytes occupied by the atom header.) In this case, the atom is a movie data atom, whose type is 'radar'. The data contained in a movie data atom is one or more media samples, which together comprise the movie's media data. A media sample is a single element of movie data. (You can think of a video frame as a single sample.) Now let's turn our attention to the resource fork in Figure 6.2. As you can see, the resource fork contains both a resource header and a resource m a p (which are contained in every well-formed Macintosh resource file). The resource fork also contains a resource of type 'moor', which is called the movie resource. The movie resource contains the movie metadata, w h i c h - - l o and behold--is structured as an atom of type 'moor'. This atom is called the movie atom. The main problem with double-fork movie files is that they cannot easily be transported to operating systems that do not support resource forks. To facilitate cross-platform deployment of multimedia content, QuickTime also supports single-fork movie files. In this case, the movie media data and the movie metadata are stored in one file (which is the data fork on Macintosh systems). ~ p i c a l l y , the movie atom is simply appended to the end of the movie data atom, as shown in Figure 6.3. This movie file can be opened and played back on any operating system that supports the QuickTime playback software. As a result, we shall always build single-fork movie files w h e n e v e r we create QuickTime movie files. Before we move on, let's take a m o m e n t to try to prevent some potential confusion. As we've just seen, both single- and double-fork movie files store the movie metadata in an atom of type 'moor', called the movie atom. In a double-fork movie file, the movie atom is contained in a resource of type 'moor ~ called the movie resource. Indeed, in a double-fork movie file, the
The Structure of QuickTime Movie Files 155
Figure 6.3 A single-fork movie file.
movie atom and the movie resource are identical. This fact has encouraged some developers to use the terms "movie resource" and "movie atom" interchangeably. But this is unfortunate, since (as you can see) there is no movie resource in a single-fork movie file, but there is a movie atom. The potential for confusion is c o m p o u n d e d by the fact that the Movie Toolbox includes the function AddMovieResource, which we will need to call to add the movie atom to our single-fork movie files (see, for instance, Listing 6.1). Originally, QuickTime did not provide a way to create single-fork movie files directly. Instead, you had to create a double-fork movie file and then call the function FlattenMovieData to make a single-fork copy of that double-fork movie file. In QuickTime 3.0, AddMovieResource was revised to support direct creation of single-fork movie files. Nowadays, AddMovieAtom would perhaps be a better name than AddMovieResource.
Fast S t a r t M o v i e Files Notice that, according to the default structure of single-fork movie files (as shown in Figure 6.3), the movie atom is stored at the end of the file. The reason for this is quite simple. Typically, we build a new QuickTime movie file by creating an empty movie file, adding new tracks to the movie, adding a new media to the track, and then adding some media samples to the media. As this process continues, the Movie Toolbox keeps track of the gradually changing metadata, which can be added to the movie file only after all the movie data has been added. The movie metadata is rather like a cargo manifest, which can be deemed accurate only after all the cargo has been loaded. In some cases, this default location of the movie atom is unproblematic. When the movie file is local (that is, stored on a local hard disk, CD-ROM, or other random-access storage device), QuickTime can search through the file quickly enough that it can find the movie m e t a d a t a - - w h i c h it needs in order to start playing the movie--with no appreciable delay. But w h e n the movie file is being sent across a local area network, or indeed across the Internet, it isn't generally possible to randomly access the file's data in this manner. This means that QuickTime cannot start playing remotely stored movies until the entire movie has been downloaded from the remote storage device to the local playback machine.
156
Chapter 6 Doug's 1st Movie
To remedy this situation, QuickTime provides support for creating Fast Start movie files, in which the movie atom is stored as the first atom in a single-fork movie file. Figure 6.4 shows the structure of a Fast Start movie file. As you can see, the only difference between a Fast Start movie file and a default single-fork movie file is the location of the movie atom. This simple change, however, is enough to enable QuickTime to start playing movies served from the Web via HTTP or FTP before the entire movie has downloaded.
Figure 6.4 A Fast Start movie file. To create a Fast Start movie file, we need to create a single-fork movie file and t h e n call VlattenMovieOata with the flattenForceMovieaesourceBeforeMovieData flag. We'll see how to do this later.
R e f e r e n c e a n d S e l f - C o n t a i n e d M o v i e Files Earlier, I warned that a movie file might not actually contain the movie's media data, but only references to the media data, which is contained in some other file (called the media lile). A file that has this characteristic is called a reference movie file. Figure 6.5 illustrates this possibility. Here, the movie file itself contains only the movie atom, which contains references to the media data. Reference movie files can be useful if you want to share some data among two or more QuickTime movies. By referencing the shared media data instead of containing copies of it, the associated QuickTime movie files can take up substantially less space.
Figure 6.5 A reference movie file and its target media file. The Structure of QuickTime Movie Files 1 5 7
It's also possible for a reference movie file to refer to media data in more than one media file. Indeed, it's possible for a reference movie file to refer to media data in some media file as well as media data contained in the reference movie file itself. {The first kind of media reference is called an external reference, and the second kind is called an internal reference.) QuickTime really doesn't care w h e r e the media data is stored, as long as it can resolve the references to that data at playback time. The opposite of a reference movie is a self-contained movie/ile. In a selfcontained movie file, all the media data needed for playing the movie is contained in the movie file itself. Or, put another way, a self-contained movie file does not depend on any other files. Put yet another way, all media references in a self-contained movie file are internal references. Figure 6.6 shows a self-contained movie file, in w h i c h the media references in the movie atom are resolved to media data in the movie file itself.
Figure 6.6
A self-contained movie file.
If we've got a reference movie file, we can create a self-contained movie file from it by calling FlattenMovieData. {Note, however, that it's possible to build a self-contained movie file directly, without having to create a reference movie file and then call FlattenMovieData to create another movie file.) FlattenMovieData resolves all media references and copies the referenced data into the movie file itself. For this reason, a self-contained movie file is sometimes also called a flattened movie file. But this terminology can be confusing, since FlattenMovieData also makes copies of any media data that is multiply referenced by internal media references. That is to say, if a self-contained movie file contains several internal references to the same media data, then the result of calling FlattenMovieData on that file will be a larger movie file that contains several copies of the referenced media data (each of which is referred to exactly once by the movie atom). So, if we reserve the term "flattened" for any movie files that might have been created by FlattenMovieData, it follows that not all self-contained movie files are flattened movie files. To make matters worse, the term "flattening" is sometimes also used to refer to the process of converting a double-fork movie file into a single-fork movie file. So, in this parlance, a flattened movie file is simply any single-
158
Chapter 6 Doug's 1st Movie
fork movie file, whether or not it contains any external media references. Perhaps the best course of action is simply to avoid using the term "flattened" altogether.
I n t e r l e a v e d a n d N o n - i n t e r l e a v e d M o v i e Files We've almost reached the end of our survey of QuickTime movie file organizations. There is only one final twist that we need to consider. Typically, w h e n we build a movie file that contains more than one kind of media data, we add the media samples for one kind of media data [for example, video data) and then we add the media samples for another kind of media data (for example, audio data). The resulting file is a non-interleaved movie file, as shown in Figure 6.7. Non-interleaved movie files do not provide optimal playback from slower storage devices {such as CD-ROMs), where seeking can be expensive, or in situations [such as network playback) where random access to the file data is not possible. Instead, it's better to reorganize the media data so that it's arranged in temporal order. That is to say, we'd like the audio and video data for the first second of the movie to be stored close to one another, followed by the audio and video data for the next second of the movie, and so forth. Figure 6.8 shows an interleaved movie tile that has the desired structure. To create an interleaved movie file, we typically create a single-fork movie file and then call F] attenMovieData. By default, F] attenMovi eData interleaves the media data to ensure optimal movie playback. You can override this default behavior by passing the f]attenDontInter]eaveF]atten flag to F]attenMovieData; the result would be a self-contained, non-interleaved movie file.
Figure 6.7
A non-interleaved movie file.
Figure 6.8
An interleaved movie file.
The Structure of QuickTime Movie Files 1 5 9
CreatJng QuiclCTJme Movie Files Our ultimate goal, then, is to create a single-fork, self-contained, Fast Start, interleaved movie file. The resulting file will play on all platforms that support QuickTime movie playback and be suitable for distributing on CDROM and over the Internet. To simplify things, we're going to create a movie file that contains a single video track. We'll postpone adding other kinds of tracks to future chapters. To create a single-fork, self-contained QuickTime movie file, we typically need to perform eight operations, in this order: 1. Create a new, empty movie file and a movie that references that file (CreateMovi eFi I e).
2. Add a new track to that movie (NewMovieTrack). 3. Add a new media to that track (NewTrackMedia). 4. Add media samples to the media. 5. Insert a reference to the media segment into the track at the desired offset (InsertMedi alntoTrack).
6. Add the movie atom to the movie file (AddMovieResource). 7. Close the movie file (Cl oseMovieFi I e). 8. Dispose of the movie (Di sposeMovie). If we want the final movie file to be either Fast Start or interleaved, we need to perform one additional operation: 9. Place the movie atom as the first atom in the movie file, and interleave the media data (FI attenMovieData). As you can see, each of these steps--except for step 4--can be accomplished with a single Movie Toolbox function. We're going to use these eight functions, in this order, over and over again as we build different kinds of QuickTime movie files, here and in future chapters. So you should take a moment to burn these steps into your memory. The only real differences between building, say, a video movie file and a wired sprite movie file concern step 4, adding media samples to the media. Even though the movies in those two kinds of files have radically different playback characteristics, the fundamental structure of the QuickTime movie files is the same, and so the way we build those files will be essentially the same. Let's take a preliminary look at how this all comes together in practice. Listing 6.1 defines the function OTMM_CreateVideoMovie, which is the routine used by our sample application QTMakeMovie for building a QuickTime movie file. QTMM_CreateVideoMovie begins by calling the framework function
160
Chapter6 Doug's 1st Movie
QTFrame_PutFile to elicit from the user a filename and location for the new movie file. Then QTMM_CreateVideoMovie calls the Movie Toolbox functions just listed to build a movie file. Step 4, adding media samples to the media, is handled by the application function QTMM_AddVideoSampl esToMedi a, which is sandwiched between the t w o calls Begi nMedi aEdi ts and EndMediaEdi ts.
Listing 6.1 Creating a QuickTime movie. OSErr QTMM_CreateVideoMovie (void) Movie Track Medi a FSSpec Boolean Boolean StringPtr StringPtr long short short OSErr
myMovie = NULL; myTrack = NULL; myMedia = NULL; myFile; myI sSel ected = false; mylsReplacing = false; myPrompt = QTUti I s ConvertCToPascal Stri ng (kNewMoviePrompt) ; myFi I eName = QTUti I s_ConvertCToPascaI St r i ng( kNewMovieFi I eName) ; myFlags = createMovieFileDeleteCurFile I createMov i eFi I eDontCreateRes Fi I e; myResRefNum = O; myResID = movielnDataForkResID; myErr = noErr;
/ / prompt the user for new filename QTFrame_PutFile(myPrompt, myFileName, &myFile, &mylsSelected, &mylsReplacing) ; myErr = mylsSelected ? noErr 9userCanceledErr; i f (myErr l= noErr) goto bai I ; / / create a movie f i l e for the destination movie myErr = CreateMovieFile(&myFile, sigMoviePlayer, smCurrentScript, myFlags, &myResRefNum, &myMovie); i f (myErr l= noErr) goto bai I ; / / create the movie track and media myTrack = NewMovieTrack(myMovie, Fi xRati o (kVi deoTrackWi dth, 1), Fi xRati o (kVi deoTrackHei ght, 1), kNoVoI ume) ; myErr = GetMoviesError() ; i f (myErr i= noErr) goto bai I ;
Creating QuickTime Movie Files 161
myMedia = NewTrackMedia(myTrack, VideoMediaType, kVideoTimeScale, NULL, 0); myErr = GetMoviesError() ; i f (myErr ! = noErr) goto bai I ; / / create the media samples myErr = BeginMediaEdits(myMedia) ; i f (myErr ! = noErr) goto bai I ; myErr = QTMM_AddVideoSamplesToMedia(myMedia, kVideoTrackWidth, kVideoTrackHeight); i f (myErr ! = noErr) goto bai I ; myErr = EndMediaEdits(myMedia) ; i f (myErr ! = noErr) goto bai I ; / / add the media to the track myErr = InsertMedialntoTrack(myTrack, O, O, GetMediaDuration(myMedia), f i x e d l ) ; i f (myErr ! = noErr) goto bai I ; / / add the movie atom to the movie f i l e myErr = AddMovieResource(myMovie, myResRefNum, &myResID, NULL); bail if if
9 (myResRefNum l= O) Cl oseMovi eFi I e (myResRefNum) ; (myMovie ! = NULL) Di sposeMovi e (myMovie) ;
free (myPrompt) ; free (myFi I eName) ; return (myErr) ;
In the following subsections, we'll gradually dissect Listing 6.1.
162
Chapter6 Doug's 1st Movie
Creating a N e w M o v i e File The first thing we need to do to create a QuickTime movie file is create a new, empty QuickTime movie file. We can do this by calling the CreateMovi eFile function, like this: myFlags = createMovieFileDeleteCurFile [ createMovieFileDontCreateResFile; myErr = CreateMovieFile(&myFile, sigMoviePlayer, smCurrentScript, myFlags, &myResRefNum, &myMovie);
Here, myFi l e is a file system specification that indicates the name and location of the new movie file. The second parameter specifies the creator type for the new file; in this case, we're passing the constant sigMoviePlayer (defined in the file Fi 1eTypesAndCreators.h) to indicate that we want the file's creator to be the QuickTime Player application (n6 MoviePlayer). Of course, you are free to use any other creator type that you wish. The third parameter specifies the script in which the movie should be created; here, as always w h e n we need to specify a script system, we pass the constant smCurrentScript. The fourth parameter to CreateMovieFi l e is a set of flags that modify the default behavior of CreateMovieFile. Generally, we will pass the two flags indicated, createMovi eFi 1eDel eteCurFi 1e and createMovi eFi 1eDontCreateResFile, which tell CreateMovieFile to delete any file already having the name and location specified by the myFi l e parameter and to create a single-fork movie file. The last two parameters allow CreateMovieFile to pass information back to us, if it successfully creates the specified movie file. In the fifth parameter, it returns to us the file reference n u m b e r for the opened movie file. (Don't let the parameter name myResRefNum fool you; because we passed the create~ MovieFileDontCreateResFile flag, the opened file fork is a data fork, not a resource fork.) CreateMovieFile accepts a flag that tells it not to open the newly created file, but here we want it to open the file so that we can add data to it. In the final parameter, CreateMovieFi l e passes back to us an identifier for a new, empty movie that references the file we just created. The parameter myMovi e is of type Movi e, which is precisely the sort of in-memory QuickTime movie that we've been using in previous chapters. From here on in, we w o n ' t do anything directly to the movie file itself. Rather, we're going to build the movie (by adding tracks and media samples to it) and then later use the AddMovieResource function to write the movie atom into the movie file.
Creating QuickTime Movie Files 163
Adding Tracks to a Movie Once w e ' v e called CreateMovieFile to create a new movie file and a new, empty movie, we need to add a track to the movie. We do this by calling the NewMovieTrack function, like this: myTrack = NewMovieTrack(myMovie, Fi xRati o (kVi deoTrackWi dth, I), FixRatio(kVideoTrackHeight, 1), kNoVolume); NewMovieTrack needs to know which movie to add the new track to; here we pass the movie identifier that we obtained from the call to CreateMovieFile. NewMovieTrack also needs to k n o w the dimensions of the new track. Since w e ' r e building a movie from scratch, we'll specify some predefined dimensions. The height and width are expected to be in units of type Fixed, so we'll use the FixRatio function to convert the predefined integer height and widths to that type. In the present case, the track is 202 pixels high and 152 pixels wide: #define kVideoTrackHeight #define kVideoTrackWidth
202 152
(These are the dimensions of a picture stored in the resource fork of the QTMakeMovie application.) The final p a r a m e t e r to NewMovieTrack is a value that specifies the desired volume level of the new track. Because the track we are about to create is a video track, we pass the constant kNoVo]ume to indicate that the volume level should be 0.
Adding a Media to a Track Recall that a track is associated with exactly one media structure (or media). The media structure contains information about the type and location of the actual media samples that comprise the QuickTime movie data. We create a new media and associate it with an existing track by calling the NewTrackMedia function, like this: myMedia = NewTrackMedia(myTrack, VideoMediaType, kVideoTimeScale, NULL, 0);
The first parameter is the track with which the new media is to be associated; here of course we use the track that we just created by calling NewMovieTrack. The second p a r a m e t e r specifies the type of media we w a n t to
164
Chapter6 Doug's 1st Movie
create. As you can see, we've used the constant VideoMediaType to specify that we want to create a new video media. The Movie Toolbox defines constants for all media types for which QuickTime supplies a media handler (that is, a component that knows how to interpret a specific kind of media data). Here are a few of the c o m m o n media types defined in Movies .h: enum { VideoMediaType SoundMediaType TextMedi aType MusicMediaType TimeCodeMediaType Spri teMediaType FlashMediaType MovieMediaType TweenMediaType
};
= = = = = = = = =
FOUR CHAR CODE( ' v i d e ' ) FOUR_CHAR_CODE(' soun ) FOUR_CHAR_CODE(' text ) FOUR CHAR CODE('musi) FOUR CHAR CODE('tmcd) FOUR_CHAR_CODE(' sprt ) FOUR CHAR CODE( ' f l s h ) FOUR CHAR CODE('moov) FOUR CHAR CODE(' twen ) D
D
Over time, we will turn our attention to each of these media types and see how to build a movie that contains media data of that type. For the m o m e n t , as you know, we're focussing on building a movie with video media data. The third parameter to the NewTrackMedi a function specifies the media time scale, which is the n u m b e r of media time units that elapse per second. The media time unit defines the media time coordinate system. The media time coordinate system is independent of the movie time coordinate system, which we discussed briefly earlier. In addition to interpreting the media data, the media handler for a track's media also manages the task of mapping values from the movie time coordinate system to the media time coordinate system. For example, w h e n the movie is ready to display a video frame, it passes the current movie time to the video media handler, w h i c h converts that time into the video media time coordinate system and then retrieves the video data at that media time. The movie time coordinate system is independent of the media time coordinate system largely to allow each media handler to work with time units that are appropriate to the kind of media it is handling. Video media often uses a time scale of 600, which is evenly divisible into the most c o m m o n frames-per-second values (8, 10, 12, 15, 24, 30, and 60). Sound media typically uses a time scale equal to the sample rate of the sound, so some comm o n sound media time scales are 11,025 (11 kHz), 22,254.54 (22 kHz), and 44,100 (44 kHz). The default QuickTime movie time scale is 600. This value was selected to facilitate the mapping between the movie time coordinate system and the
Creating QuickTime Movie Files 1G5
video media time coordinate system. Our sample application Q T M a k e M o v i e uses 600 as the video media time scale: #define kVideoTimeScale
600
/ / 600 units per second
The fourth p a r a m e t e r to NewTrackMedia specifies a media data reference, w h i c h identifies the file (or Other media container) that is to hold the m e d i a data for the specified track. Here we pass the value NULLto indicate that the media data is to be stored in the file associated with the movie (that is, the file created w h e n we called CreateMovieFi l el. It w o u l d be possible to specify some other file to hold the media data for this track; however, since we w a n t to create a self-contained file, we'll pass NULLhere. For the same reason, we pass 0 in the fifth parameter. If we were passing a non-NULL media data reference in the fourth parameter, we w o u l d use the fifth p a r a m e t e r to specify the type of data reference.
Adding Samples to a Media We've been occupied so far with creating the movie m e t a d a t a - - t h e "bookkeeping details"--that will end up inside the movie atom in the Q u i c k T i m e movie file. What we need to do now is add some media data to the movie file. The media data is w h a t the user will ultimately see or hear w h e n the movie is played and w h a t we would normally think of as the "meat and potatoes" of the QuickTime movie file. As we saw earlier, the media data is comprised of one or more media samples. To add samples to an existing media, we need to open a media editing session. That is to say, we need to inform the Movie Toolbox that we w a n t to change the media samples referenced by a track's media. We can do this by calling the Begi nMedi aEdi ts function, specifying the media we want to edit: myErr = BeginMediaEdi ts (myMedia) ;
W h e n we call BeginMediaEdits, the Movie Toolbox opens the media container, if it isn't already open, and makes sure that we can add samples to it. In the present case, the media container is the QuickTime movie file that we created w h e n we called CreateMovieFi le. Finally, w e ' v e done all the g r o u n d w o r k that we need to do before we can add some media data to our movie file. We've created a movie, a track, and a media, and w e ' v e opened a media editing session. All that remains is to call AddRediaSample one or more times to add some media samples to the media container. Unfortunately, we haven't created any media samples yet. For the m o m e n t , let's defer going into those details, so that we can spend the r e m a i n d e r of this section focussing on the overall m o v i e m a k i n g process.
1GG Chapter6 Doug's 1st Movie
Let's just call an application-defined function that adds the desired video media samples to the media" myErr = QTMM_AddVideoSamplesToMedia (myMedia, kVideoTrackWidth, kVideoTrackHeight) ;
We'll spend the entire next section, "Adding Media Samples," walking through the function QTMM_AddVideoSamp] esToMedi a. W h e n we are finished adding samples to the media, we close the media editing session by calling the EndMediaEdi ts function, like this: myErr = EndMediaEdits(myMedia) ;
Inserting a M e d i a Segment Once we've finished adding samples to the media and ended the media editing session, we need to add a reference to some or all of that media to the track. In particular, we need to specify w h e n in the track the media segment is to begin; we also need to specify the portion of the media samples to be inserted into the track. We can handle all of this with the InsertMediaIntoTrack function" myErr = I nsertMedi alntoTrack (myTrack, / / time in track O, / / beginning of media segment O, GetMedi aDurati on (myMedia), / / duration of media segment fixed1) ;
We're not doing anything fancy here, but there are a few things to keep in mind. The second parameter indicates where in the specified track the media data is to be inserted. The value of that parameter is interpreted in the movie time scale. So, if we want the video samples to start playing 2 seconds after the movie begins and if the movie time scale is 600, then we would pass the value 1200 in the second parameter. In the present case, we want the video samples to start at the beginning of the movie, so we pass 0. The second and third parameters specify the beginning and duration of the media segment to which we want to insert a reference into the target track. Both of these values should be expressed in the media time scale. Since we want to insert a reference to the entire media data, we set the beginning time to 0 and the duration to the value returned by the GetMediaDurati on function. The last parameter to InsertMediaIntoTrack indicates the media rate, which is the rate at which the media samples are to be played. We want the
Creating QuickTime Movie Files 167
samples to be played at their natural playback rate, so we pass the constant fixed1.
Adding the Movie Atom Now that we've inserted a reference to the media data into the movie's single video track, w e ' v e finally collected all the movie metadata we need, so we can add the movie atom to the QuickTime movie file. We do this by calling the AddMovieResource function, like this: myErr = AddMovieResource(myMovie, myResRefNum, &myResID, NULL);
AddMovieResource takes the information contained in the movie myMovie and creates a movie atom, which it then appends to the file specified by the myResRefNum parameter. Originally, the third and fourth parameters specified the resource ID n u m b e r and the resource n a m e for the movie resource (which, you'll recall, was added to the file's resource fork as a resource of type 'moov'). Nowadays, we are no longer interested in creating double-fork movie files, so we do not need to pass a movie resource name. Moreover, the Movie Toolbox now supports the constant movieInDataForkResID for the resource ID parameter, which indicates that the movie atom should be added to the data fork, not to the resource fork. So before we call AddMovieResource, we'll set the desired resource ID, like this: myResID = movielnDataForkResID;
Finishing Up We are essentially finished constructing a QuickTime movie file. Only two minor housekeeping tasks remain: we need to close the movie file that the Movie Toolbox automatically opened for us w h e n we called CreateMovieFi le, and we need to dispose of the movie that CreateMovieFile returned to us {and to which we subsequently added a video track). The function OTMM_ CreateVideoMovie ends with these lines of code: i f (myResRefNum != O) Cl oseMovieFi I e (myResRefNum) ; i f (myMovie I: NULL) Di sposeMovie (myMovie) ;
168
Chapter6 Doug's 1st Movie
Adding Media Samples In the previous section, we gradually dissected the function QTMM_CreateVideoMovie {defined in Listing 6.1) to learn how to create a QuickTime movie file with a video track. We postponed, however, the all-important task of adding media samples to the movie file. Instead, QTMM_CreateVideoMoviejust called the function QTMM_AddVideoSamplesToMedia, which adds some video samples to the video track's media. The reason we deferred our treatment of adding media samples is simply that a full consideration of that topic would have detracted from the main point of the previous section, which was to emphasize the general steps involved in creating any kind of QuickTime movie file. Now let's tie up the loose ends by seeing how to add some video samples to the media of the movie's track. To add a sample to a media, we really need only two things. First, of course, we need the media sample data. For a video track, the sample data usually consists of a compressed image. For a sound track, the sample data usually consists of some digitized (and possibly compressed) audio data. For a text track, the sample data consists of some n u m b e r of characters. And so forth, for all the other kinds of media supported by QuickTime. Second, we need a concise description of the kind of data contained in the media sample. The media type (specified in the call to NewTrackMedia) is generally not sufficient to allow the media handler to know how to process the media samples. For instance, a video track can contain images compressed using any one of a large number of video codecs {compressors/ decompressors}, and the track can have virtually arbitrary dimensions. For the video media handler to be able to display the video media samples correctly, it needs to know which compressor was used, the bit depth of the images, the dimensions of the images, and a handful or so of other pieces of information. The AddMediaSample function expects us to pass it this information in a structure called a sample description. The file Movies.h defines a sample description like this: struct SampleDescription { long long long short short
descSize; dataFormat; resvdl; resvd2; dataReflndex;
}; The only two fields that we usually need to fill in are descSize, which specifies the size of the sample description, and dataFormat, which specifies the codec that compressed the data. But except for some very simple media
Adding Media Samples 169
types, this structure isn't complete enough. Instead, for video media data, we'll fill in an image description, defined in ImageCompression.h like this" struct ImageDescription { long CodecType long short short short short long CodecQ CodecQ short short Fixed Fixed long short Str31 short short
idSize; cType; resvdl; resvd2; dataReflndex; version; revi sionLevel ; vendor; temporaI Qual i ty; spatiaIQual ity; width; height; hRes; vRes; dataSize; frameCount; name; depth; clutID;
}~ As you can see, the first five fields of an image description are of the same type as the first five fields of a sample description {at least if you know that CodecType is defined as long). In effect, an image description is a sample description that contains some extra fields to hold information about video media. So, when we call AddMediaSample to add a video media sample, we'll pass it an image description, suitably cast to a sample description. But we're not ready to call AddMediaSample quite yet. First we need to create some video media data, the compressed images that, when decompressed, become the frames of our movie. Let's draw some images and compress them.
Drawing Video Frames Our video media data is going to consist of a series of individual compressed images. We are of course free to use any images we like. For fun, let's make the first image in the series a completely white rectangle, the last image in the series a copy of the QuickTime penguin picture, and the intermediate images a progressively greater blend of the first and last images. Figure 6.9 shows the first and last movie frames, together with three intermediate
170
Chapter6 Doug's 1st Movie
Figure 6.9 Someframes of the final movie file.
frames. (In technical jargon, we're going to create a cross-fade between the first and last images.) Listing 6.2 defines the QTMM_DrawFramefunction, which draws a frame of a specified height and width into a specified offscreen graphics world. QTMM_DrawFramealso takes as a parameter the sample number; it uses that number to determine how much blending of the first and last images to perform. Listing 6.2 Drawing a frame of the movie. static void QTMM_DrawFrame (short theTrackWidth, short theTrackHeight, long theNumSample, GWorl dPtr theGWorl d) Handle char static PicHandle static GWorldPtr static Graphics ImportComponent Rect RGBColor ComponentResul t
myHandl e = NULL; myData [kPICTFi I eHeaderSi ze] ; myPicture = NULL; myGWorld = NULL; mylmporter = NULL; myRect; myColor; myErr = noErr;
MacSetRect(&myRect, O, O, theTrackWidth, theTrackHeight); i f (myPicture == NULL) { myErr = NewGWorld(&myGWorld, kPixeIDepth, &myRect, NULL, NULL, (GWorldFlags)O); i f (myErr ! = noErr) goto bai I ; / / read a picture from our resource f i l e myPicture = GetPicture(kPicturelD) ; i f (myPicture == NULL) goto bai I ;
Adding Media Samples 171
/ / use Munger to prepend a 512-byte header onto the picture data; / / this converts the PICT resource data into in-memory PICT f i l e data myHandle = (Handle)myPicture; Munger(myHandle, O, NULL, O, myData, kPICTFileHeaderSize); / / get a graphics importer for the picture myErr = OpenADefaultComponent(Graphics ImporterComponentType, kQTFileTypePicture, &mylmporter) ; i f (myErr ! = noErr) goto bai I ; / / configure the graphics importer myErr = GraphicslmportSetGWorld(mylmporter, myGWorld, NULL); i f (myErr ! = noErr) goto bai I ; myErr = GraphicslmportSetDataHandle(mylmporter, myHandle) ; i f (myErr I= noErr) goto bai I ; myErr = GraphicslmportSetBoundsRect(mylmporter, &myRect); i f (myErr ! = noErr) goto bai I ; / / draw the picture into the source GWorld myErr = GraphicslmportDraw(mylmporter) ; i f (myErr i= noErr) goto bai I ; / / set the blend amount (0 = f u l l y transparent; Oxffff = f u l l y opaque) myColor.red = (theNumSample- 1) * (Oxffff / kNumVideoFrames- 1); myColor.green-- (theNumSample- 1) * (Oxffff / kNumVideoFrames- 1); myColor.blue = (theNumSample- 1) * (Oxffff / kNumVideoFrames- 1); OpColor(&myCol or) ; / / blend the pi cture (in the source GWorld) into the empty rectangle / / (in the dest ination GWorld) CopyBi ts ( (Bi tMapPtr)*GetGWorl dPi xMap(myGWorld), (BitMa pPtr) *GetGWorl dPi xMap(t heGWorld), &myRect, &myRect, blend, NULL);
'1172 Chapter 6 Doug's 1st Movie
if
(theNumSample == kNumVideoFrames)
goto bai I ;
return; bail 9 i f (myHandle != NULL) Di sposeHandl e (myHandle) ; i f (myPicture != NULL) Rel easeResource ( (Handl e)myPi cture) ; i f (mylmporter l: NULL) Cl oseComponent(mylmporter) ;
I won't say much more about QTMM_DrawFrame, because it's not directly concerned with building QuickTime movie files. Basically, it reads an image from a 'PICT' resource, attaches a graphics importer to that image, draws that image into an offscreen graphics world, and then blends that image with the all-white image in the offscreen graphics world passed to QTMM_DrawFrame. You can work through the function if you like, or you can just trust me that, when passed values from 1 to kNumVideoFrames, it generates a series of images that includes the five shown in Figure 6.9.
Compressing Video Frames Now that we've got an image that we want to include as a media sample of our QuickTime movie file, we need to compress it so that it takes up less space on disk. If we didn't do any compression on the video frames, each 152-by-202-pixel frame would occupy about 122,000 bytes, so our entire lO0-frame movie file would occupy about 12 Mbytes. That's a bit large for a l O-second movie. By compressing the movie frames, we can reduce the size of the final movie file to around 470 Kbytes (using ]PEG compression) with no appreciable loss of image quality. The only downside to compressing the movie frames [aside from any image degradation introduced by the image compressor) is that it takes time to compress the frames while building the movie file and it also takes time to decompress the frames during movie playback. But decompressing compressed video media samples is almost always faster than reading larger chunks of uncompressed data from a storage device, and it's orders of magnitude faster than receiving larger chunks of data across a network connection. So compression not only reduces the amount of storage required by a
Adding Media Samples '1173
movie file, it also enhances playback performance by shifting much of the work from the storage device or network connection to the CPU. QuickTime provides a rich set of services for compressing and decompressing images and sequences of images, principally provided by the Image Compression Manager (or, more briefly, the ICM). Right now we are concerned only with compressing our images into video media samples, and we can handle this by using only two ICM functions, GetMaxCompressionSize and Compresslmage. GetMaxCompressionSize tells us the maximum size of our compressed image, given parameters that specify the size of the image to be
compressed, the compressor, and the desired level of compression. We will use that maximum size when allocating a data buffer, into which compress Image will write the compressed image. As a bonus, CompressImage also returns to us an image description whose fields have been set to the correct values for the image it has compressed. As we've seen, we'll need that image description when we eventually call AddMediaSample. The first thing we need to do, before we can draw or compress our image, is create a new offscreen graphics world, which will hold the image drawn by OTMM_DrawFrame. So OTMM_AddVideoSamplesToMedia begins with these lines of code" MacSetRect(&myRect, O, O, theTrackWidth, theTrackHeight); myErr = NewGWorld(&myGWorld, kPixeIDepth, &myRect, NULL, NULL, (GWorl dFl ags)O) ; i f (myErr l= noErr) goto bai I ; myPixMap = GetGWorldPi xMap(myGWorld) ; i f (myPixMap == NULL) goto bai I ; LockPi xel s (myPixMap) ;
There is nothing fancy here; we just set the rectangle myRect to the width and height of the movie frame and call NewGWorld to create an offscreen graphics world of that size. Then we retrieve the pixel map associated with that graphics world and lock the pixel map. Both GetMaxCompressionSize and compress Image operate directly on pixel maps, not on offscreen graphics worlds. Now we can call GetMaxCompressionSize, like this: myErr = GetMaxCompressionSi ze (myPixMap, &myRect, O, / / let ICM choose depth
174
Chapter 6 Doug's 1st Movie
codecNormal Qual i ty, myCodecType, (CompressorComponent) anyCodec, &myMaxComprSi ze) ;
As you can see, we pass GetMaxCompressionSi ze the pixel map, the image rectangle, an image bit depth, a compression quality specifier, the desired compression type, a codec specifier, and a pointer to a long integer. GetMaxCompressionSize will update that long integer with the maximum number of bytes required to hold an image having the specified characteristics compressed using the specified compressor. Once we know the maximum size a compressed image will occupy, we can allocate a buffer to hold the compressed image data, by calling NewHandle: myComprDataHdl = NewHandle (myMaxComprSize) ; i f (myComprDataHdl == NULL) goto bai I ; HLockHi (myComprDataHdl) ; myComprDataPtr = *myComprDataHdl ;
Notice that we lock the handle and then dereference it to obtain a pointer to the data buffer. That's because Compress Image wants us to pass it a pointer, not a handle. We are almost ready to call Compress Image. We need only one more thing, namely, a handle to an image description. As I mentioned earlier, compress Image will fill in the fields of the image description we pass it; it will also resize the image description as necessary, so we'll just pass it a handle to a four-byte block of memory: myImageDesc = ( ImageDescri pt i onHandl e) NewHandle (4) ;
After we've drawn an image into the offscreen graphics world myGWorld, we can compress that image by calling Compress Image, like this: myErr = Compresslmage(myPixMap, &myRect, codecNormaI Qual i ty, myCodecType, mylmageDesc, myComprDataPtr) ; Compress lmage takes as parameters the pixel map of the offscreen graphics world, a rectangle that specifies the portion of the image to compress, the
Adding Media Samples 175
same compression quality and compressor type that we previously passed to fietMaxCompressionSize, the image description, and the pointer to a buffer. If all goes well, myComprOataPtr will point to the compressed image data and myImageOesc will be resized and updated. The actual size of the data buffer can be found by inspecting the dataSi ze field of the image description.
Adding Video Frames to a Media We can finish this all off with one more step. Finally, we are ready to call AddMediaSample to add the compressed image data to the media as a video media sample: myErr = AddMediaSample(theMedi a, myComprDataHdl, O, / / no offset in data (**my ImageDesc). dataSi ze, kVideoFrameDuration, / / frame duration (SampI eDescri pt i onHandI e) myImageDesc, 1, / / one sample O, / / self-contained samples NULL);
The first two parameters here are the media to which we want to add the sample and the sample data itself. The third parameter specifies the byte offset into the sample data buffer at which AddMediaSample should start reading the sample data; since the entire buffer comprises the sample data, we specify 0 as the byte offset. The fourth parameter indicates the number of bytes to be copied into the movie file. As we just saw, we can use the dataSi ze field of the image description to obtain that size. The fifth parameter specifies the duration, in the media time scale, of the sample being added to the media. We are giving each frame of the movie the same duration, which is specified by the constant kVideoFrameDuration: #define kVi deoFrameDuration
kVi deoTimeScal e/lO
Each frame of the movie has a duration of 1/10th of a second. Since there are 100 frames in the movie, the entire movie will last 10 seconds. The sixth parameter to AddMediaSamp] e is a handle to a sample description. As predicted earlier, we can just cast our image description handle to a sample description handle. The seventh parameter indicates the number of samples contained in the data buffer. The eighth parameter is a set of flags, which we can leave set to the default value of 0. The last parameter is a
17G Chapter6 Doug's 1st Movie
pointer to a time value, in w h i c h AddMediaSample w i l l return the media time
at which the sample was added. We don't care about that information, so we pass NULL. Listing 6.3 gives a complete definition of the QTMM_AddVideoSamp] esToMedi a function, which adds 100 samples to the specified media, thereby creating the movie data for our QuickTime movie file. Listing 6.3 Adding samples to a media. OSErr QTMM_AddVideoSamplesToMedia (Media theMedia, short theTrackWidth, short theTrackHeight) GWorldPtr Pi xMapHandle CodecType long long Handle Ptr ImageDescri pti onHandl e CGrafPtr GDHandl e Rect OSErr
myGWorld = NULL; myPixMap = NULL; myCodecType = kJPEGCodecType; myNumSample; myMaxComprSize = OL; myComprDataHdl = NULL; myComprDataPtr = NULL; myImageDesc = NULL; mySavedPort = NULL; mySavedDevice = NULL; myRect; myErr = noErr;
MacSetRect(&myRect, O, O, theTrackWidth, theTrackHeight); myErr = NewGWorld(&myGWorld, kPixeIDepth, &myRect, NULL, NULL, (GWorldFlags)O); i f (myErr != noErr) goto bai I ; myPixMap = GetGWorldPixMap(myGWorld) ; i f (myPixMap == NULL) goto bai I ; LockPi xel s (myPixMap) ; myErr = GetMaxCompressionSi ze (myPixMap, &myRect, O, / / let ICM choose depth codecNormaIQual i ty, myCodecType, (Compres sorComponent ) anyCodec, &myMaxComprSi ze) ; i f (myErr l= noErr) goto bai I ; myComprDataHdl = NewHandle (myMaxComprSize) ;
Adding Media Samples 177
i f (myComprDataHdl == NULL) goto bai I ; HLockHi (myComprDataHdl) ; myComprDataPtr = *myComprDataHdl ; mylmageDesc = (ImageDescriptionHandle)NewHandle(4) ; i f (mylmageDesc == NULL) goto bail ; GetGWorld(&mySavedPort, &mySavedDevice) ; SetGWorld(myGWorld, NULL); for (myNumSample = 1; myNumSample <= kNumVideoFrames; myNumSample++) { EraseRect (&myRect) ; QTMM_DrawFrame(theTrackWidth, theTrackHeight, myNumSample, myGWorld); myErr = Compresslmage(myPixMap, &myRect, codecNormaI Qual i ty, myCodecType, mylmageDesc, myComprDataPtr) ; i f (myErr != noErr) goto bai I ; myErr = AddMediaSample(theMedia, myComprDataHdl, O, / / no offset in data (**my ImageDesc). dataS i ze, kVideoFrameDuration, / / frame duration (SampI eDescri pt i onHandl e)myImageDesc, 1, / / one sample O, / / self-contained samples NULL); i f (myErr != noErr) goto bail ; bail: SetGWorld(mySavedPort, mySavedDevice) ; i f (myImageDesc ! = NULL) Di sposeHandl e ( (Handl e)mylmageDesc) ;
178
Chapter6 Doug's 1st Movie
i f (myComprDataHdl I= NULL) Di sposeHandl e (myComprDataHdl) ; i f (myGWorld ! = NULL) Di sposeGWorl d (myGWorld) ; return (myErr) ;
Saving a M o v i e We've now seen how to create a single-fork, self-contained QuickTime movie file. If we also want to create a Fast Start or interleaved movie file, we need to take one more step and call F1attenMovieData on the new movie. Our sample application QTMakeMovie does not take this extra step, but the application framework that QTMakeMovie is built upon does provide a way for us to do this. We simply need to open the new movie file and select the Save As m e n u item in the File menu. This will result in our application executing the QTFrame_SaveAsMovieFile function, which will prompt us for a new filename and location and then call FlattenMovieData to create a Fast Start, interleaved movie file. The call to F1attenMovieData looks like this: myNewMovie = Fl attenMovi eData ( myMovi e, flattenAddMovieToDataFork I flattenForceMovieResourceBeforeMovieData, &myFile, sigMoviePl ayer, smSystemScri pt, createMovieFi leDeleteCurFi le I createMovieFi leDontCreateResFi le) ;
By default, FlattenMovieData interleaves the movie data, so there is no need to do anything special to get an interleaved movie file. (Of course, since the movie we built in this chapter has only one type of media data, it's already interleaved.) We get a Fast Start movie file by specifying the constant flattenForceMovieResourceBeforeMovieData in the second parameter.
T h e Code The code folder accompanying this chapter contains the project files, source code, and resource data of the sample application QTMakeMovie, for both Macintosh and Windows. QTMakeMovie is just like our basic QTShell application, except that the Test m e n u contains a single m e n u
TheCode
179
Figure 6.10
A movie window in Mac OS X.
item, Create New Movie. When the user selects that item, Q_TMakeMovie calls the QTMM_CreateVideoMovie function (defined in Listing 6.1) to create the "appearing-penguin" movie file. In addition to showing how to create O_uickTime movie files, this code takes yet another important step forward. To wit, this code is completely Carbonized, so that the compiled application runs both under the "classic" Macintosh operating systems that support Carbon (namely, Mac OS 8 and 9) and under Mac OS X. Figure 6.10 shows a frame of the movie created by Q_TMakeMovie when opened by the application running under Mac OS X. As you can see, our movie window automatically uses the stylish new Aqua interface provided by OS X. The code changes made in QTMakeMovie to support Carbon are not very great, since I've been gradually moving toward Carbon-compliant APIs in previous versions of the application framework. The biggest changes concerned the project files for the Macintosh version of the application. As you can see in Figure 6.11, all the Macintosh-specific libraries have been replaced by the single stub library CarbonLib. (The downside here is that QTMakeMovie will not run on "classic" Macintosh operating systems that do not have the CarbonLib system extension installed.)
180
Chapter 6 Doug's 1st Movie
Figure 6.11
The project window for the Carbonized QTMakeMovie.
Conclusion In this chapter, we've made some key advances in our undersanding of QuickTime. We've gotten a taste for the basic structure of QuickTime movie files, and--more important--we've learned how to create QuickTime movie files using the high-level APIs provided by the Movie Toolbox. We'll use the same sequence of functions repeatedly throughout this book whenever we build movie files, whether they contain video, sprite, text, timecode, or other kinds of media data.
Conclusion
181
This Page Intentionally Left Blank
The Informant
Getting and Setting Movie Information
Introduction In the previous chapter, we saw how to create a QuickTime movie file that contains a single video track. We also learned a fair bit about the structure of QuickTime movies (as collections of tracks) and QuickTime movie files [as collections of atoms). In this chapter, we'll continue with the general topic of creating and configuring QuickTime movie files. We'll see how to get various pieces of information about QuickTime movies and movie files; we'll also see how to add information to a QuickTime movie to help the user determine what's in a movie. To get an idea of what we're going to accomplish here, let's suppose that we're running some version of the MoviePlayer application (the predecessor to the current QuickTime Player application). MoviePlayer's Movie menu contains the item Show Copyright. If we select that item immediately after having opened the movie file we created in the previous chapter, we'll see the movie information dialog box shown in Figure 7.1. As you can see, this is not particularly helpful. The only real "information" visible to the user is the first frame of the movie, which happens to be a solid white rectangle. It would be better to display some other frame of the movie and to add some descriptive information to the other panes of the dialog box. Figure 7.2 shows a much more useful movie information dialog box. Part of our task here will be to see how to modify the movie file so that selecting Show Copyright displays the dialog box in Figure 7.2 rather than the one in Figure 7.1. In a nutshell, this involves setting the movie poster to some frame other than the first frame, which is the default poster frame; it also involves attaching three new pieces of movie user data to the movie file. Along the way, we'll also learn how to set the preview that is contained in the file-opening dialog boxes displayed by calls to the StandardGetFi l ePreview and NavGetFi l e functions. Figure 7.3 shows a typical file-opening dialog box with a preview.
183
Figure 7.1
The movie information dialog box for our new movie.
Figure 7.2 The revised movie information dialog box for our new movie.
184
Chapter 7 The Informant
Figure 7.3
A preview contained in a file-opening dialog box.
Our sample application this time around is called QTInfo. As usual, it's based directly on the QTShell sample application that we've developed previously. QTInfo is just QTShell with one additional source code file (OTInfo.c) and some additional resources. Figure 7.4 shows the Test m e n u supported by QTInfo. As you can see, QTInfo provides the Show Copyright m e n u item, as well as a n u m b e r of other items that allow us to get and set various kinds of movie information. It turns out that we can handle the Show Copyright item with a single line of code: ShowMovielnformation(myMovie, gModalFilterUPP, OL);
Figure 7.4
The Test menu in QTInfo.
Introduction
1185
Figure 7.5 A nonmovable movie information dialog box.
The ShowMovielnformation function was introduced in QuickTime version 2.0, but has (to my knowledge) never been documented. ShowMovieInformation simply displays the movie information dialog box, which includes the movie poster image, the name of the movie, the movie's copyright information, and some other information. If you pass a universal procedure pointer to a modal dialog event filter function in the second parameter, you'll get a movable modal dialog box; otherwise, you'll get a standard nonmovable modal dialog box, as shown in Figure 7.5.
Movie Posters A movie poster image (or, more briefly, a movie poster) is a single image that represents a QuickTime movie. The images in the top-left panes of Figures 7.1, 7.2, and 7.5 are movie posters, suitably resized to fit into the available space in the movie information dialog box. A movie poster is defined by specifying a movie poster time and one or more movie poster tracks. The movie poster time specifies the time in the movie at which the image is to be found, and the movie poster tracks specify which tracks in the movie are to be used to create the movie poster. Typically a single track is used as the movie poster track, but in theory, two or more video tracks (or other tracks with visible data) could contribute to the final movie poster image. If a
186
Chapter7 The Informant
movie has no track designated as a movie poster track, then the movie won't have a poster, whatever the setting of the movie poster time. Let's see how to work with poster times and tracks.
Getting and Setting Movie Poster Times The default movie poster time is 0, which picks out the first frame in the movie. As we saw earlier, it's sometimes useful to designate some other time as the movie poster time. The function QXInfo_SetPosterToFrame, defined in Listing 7.1, sets the currently displayed movie frame to be the movie poster image. (QTInfo calls OXInfo_SetPosterXoFrame in response to the Set Poster Frame menu item.) Listing 7.1
Setting the movie poster time to the current movie time.
OSErr QTInfo SetPosterToFrame (Movie theMovie, MovieController theMC)
(
TimeValue ComponentResult
myTime; myErr = noErr;
/ / stop the movie from playing myErr = MCDoAction(theMC, mcActionPlay, (void *)OL); i f (myErr i= noErr) goto bai I ; myTime = GetMovieTime(theMovie, NULL); SetMoviePosterTime(theMovie, myTime); myErr = MCMovieChanged(theMC, theMovie) ; bail: return((OSErr)myErr) ;
}
As you can see, QTlnfo_SetPosterToFrame first calls MCDoAction to set the
movie play rate to O, which stops the movie from playing. {If the movie is already stopped, this call has no effect.} Then OTInfo_SetPosterXoFrame retrieves the current movie time by calling the fietMovieTime function and sets the movie poster time to the current movie time by calling the SetMovi ePosterTime function. QTInfo_SetPosterToFrame finishes up by calling the MCMovieChanged function, which informs the movie controller that we've made changes to the movie using the Movie Toolbox. As we've seen in past chapters, there
Movie Posters 187
are often two ways to change some characteristic of a movie: using Movie Toolbox functions and using movie controller functions. W h e n a movie is associated with a movie controller and w h e n we make a change to the movie using the Movie Toolbox, it's often necessary to keep things in sync by calling MCMovieChanged. For example, if we change the size of the movie by calling SetMovieBox, w e ' d need to call MCMovieChanged so that the movie controller can update itself appropriately. In the present case, there is no movie controller action to set the poster frame, so we used the Movie Toolbox function SetMoviePosterTime. T h e n we called MCMovieChanged on the offhand chance that the movie controller might actually care about the poster time. I've tried running QTInfo without the call to MCMovieChanged here and no h a r m appears to result, but it's better to be safe than sorry. As a general rule, if you have a movie controller associated with a movie and you use the Movie Toolbox to effect some change in the movie, call MCMovieChanged to inform the movie controller of the change. QTInfo supports the Go To Poster Frame m e n u item, which sets the current movie time to the movie poster time. Listing 7.2 defines the QTInfo_GoToPosterFrame function, which does just that.
Listing 7.2 Setting the current movie time to the movie poster time. OSErr QTInfo GoToPosterFrame (Movie theMovie, MovieController theMC)
{
TimeRecord ComponentResult
myTimeRecord; myErr = noErr;
/ / stop the movie from playing myErr = MCDoAction(theMC, mcActionPlay, (void *)OL) ; i f (myErr I= noErr) goto bai I ; / / set up a time record with the desired movie time, scale, and base myTimeRecord.value.hi = O; myTimeRecord, val ue. lo = GetMoviePosterTime(theMovie) ; myTimeRecord.base = GetMovieTimeBase(theMovie) ; myTimeRecord.scale = GetMovieTimeScale(theMovie) ; myErr = MCDoAction(theMC, mcActionGoToTime, &myTimeRecord); bai I 9 return((OSErr)myErr) ;
}
In this case, there is a movie controller action that we can use to set the current movie time, namely, mcActionGoToTime. As a result, there is no need
188
Chapter 7 The Informant
to call MCMovieChanged after we've made this change to the movie [since we made the change using movie controller actions, not the Movie Toolbox). We did of course use Movie Toolbox functions to get information needed to fill in the TimeRecord structure whose address we pass to MCDoAction, but those functions didn't make any changes to the movie; they simply gave us information about the movie.
W o r k i n g w i t h M o v i e Poster Tracks I mentioned earlier that a movie's poster image is determined both by the movie poster time and by the movie poster tracks. Each track in a movie has a track usage, which indicates w h e t h e r the track is used in the movie, the movie poster, the movie preview, or any combination of these. For instance, a movie can include a video track that consists of a single frame, and that track can be the only one in the movie that is used in the movie poster. In this way, it's possible to have a movie poster that is not simply one of the frames in the movie, but is some other image altogether. We can query a track's usage by calling the 6etTrackUsage function. GetTrackUsage returns a long integer whose bits encode the track usage. Currently, these three bits are defined: enum { t rac kUsageI nMovi e t rac kUsageI nPrevi ew trackUsagel nPoster
};
=1<<1, = 1<<2,
=1<<3
By default, a track can be used in any of these three ways, so calling GetTrackUsage on most tracks will return a value of Ox0OOOOOOE (that is, binary 1110). But we can change this default setting by calling SetTrackUsage, passing it a long integer that has the appropriate flags set or clear. We'll see some calls to GetTrackUsage and SetTrackUsage in a moment. For now, it's important to understand that a track usage value indicates a track's potential use, not its actual use. That is to say, if a particular track has a track usage value with the trackUsageInPoster flag set, the poster image might not actually include any data from that track. This might happen if the movie poster time is set to a time at which that track has no data [perhaps the track offset is greater than the movie poster time). Similarly, a track's usage value can have the trackUsagelnPreview flag set, even if the movie has no preview. To repeat, the track usage determines the uses a track can have, not the uses it actually has. Let's see how this works in practice. W h e n QTInfo wants to adjust the state of its menus, it needs to know w h e t h e r the movie in the frontmost window has a poster image. If there is no poster image, then it should disable
Movie Posters 189
the Go To Poster Frame menu item. To determine whether a movie has a poster image, QTInfo calls the QTInfo_MovieHasPoster function defined in Listing 7.3. Essentially, QTInfo_MovieHasPoster looks at each track in the movie, retrieves the track's usage value, and checks to see whether the trackUsageInPoster flag is set in that value. If there is at least one track that is capable of contributing data to the movie poster image, we'll happily count the movie as having a poster image.
Listing 7.3 Determining whether a movie has a poster image. Boolean QTInfo MovieHasPoster (Movie theMovie) long long Track long Boolean
myCount = OL; mylndex = OL; myTrack = NULL; myUsage = OL; myHasPoster = true;
/ / make sure that some track is used in the movie poster myCount = GetMovieTrackCount(theMovie) ; for (mylndex = I; mylndex <= myCount; mylndex++) ( myTrack = GetMovielndTrack(theMovie, mylndex) ; i f (myTrack == NULL) continue; myUsage = GetTrackUsage(myTrack) ; i f (myUsage & trackUsagelnPoster) break;
if
(mylndex > myCount) myHasPoster = false;
return (myHasPoster) ;
The QTInfo MovieHasPoster function is instructive for other reasons as well, in particular because it shows how to iterate through all tracks in a movie. As you can see, it begins by calling the GetMovieTrackCount function to determine how many tracks the specified movie contains. Then it repeatedly calls the GetMovieIndTrack function to get a track identifier for each of those tracks. The Movie Toolbox also supplies the GetMovieIndTrackType function, which allows us to iterate through all tracks of a specific type (say, all video tracks). We won't have occasion to use GetMovieIndTrackType in this chapter, but we will in the future.
190
Chapter 7 The Informant
Movie Previews A movie preview is a short, dynamic representation of a QuickTime movie. Typically, a movie preview is an excerpt of the movie itself (for example, the first few seconds of the movie). But, like a movie poster, a movie preview can consist of data that is not used in the normal playback of the movie. Once again, the usage values of the tracks in the movie determine the actual contents of the movie preview.
Defining Movie Previews We specify a movie preview by giving its start time, its duration, and its tracks. The recommended duration is about 3 to 5 seconds, but we are free to use a longer or shorter duration if we wish. An easy way to let the user specify a movie preview is to provide the Set Preview to Selection m e n u item, which uses the start time and duration of the current movie selection as the start time and duration of the movie preview. Listing 7.4 shows how to set the movie preview to the current movie selection.
Listing 7.4 Setting the movie preview to the current movie selection. OSErr QTlnfo_SetPreviewToSelection (Movie theMovie, MovieController theMC)
{
TimeValue TimeVal ue ComponentResult
myStart; myDurati on; myErr = noErr;
GetMovieSelection(theMovie, &myStart, &myDuration); SetMoviePreviewTime(theMovie, myStart, myDuration); myErr = MCMovieChanged(theMC, theMovie) ; return((OSErr)myErr) ;
The QTlnfo_SetPreviewToSelection function is simplicity itself. We just call GetMovieSelection to get the current movie start time and duration, and then we pass those same values to the SetMoviePreviewTime function. We need to call MCMovieChanged here because we changed the characteristics of the movie (in particular, its movie preview} using the Movie Toolbox. As we've seen, QTInfo also provides the Set Selection to Preview menu item, which sets the movie's selection to the current movie preview. Listing 7.5 defines the function QTInfo_SetSelectionToPreview, which performs this operation.
Movie Previews 191
Listing 7.5 Setting the current movie selection to the movie preview. OSErr QTInfo_SetSelectionToPreview (Movie theMovie, MovieController theMC)
{
TimeValue TimeVal ue ComponentResult
myStart; myDurat i on; myErr = noErr;
GetMoviePreviewTime(theMovie, &myStart, &myDuration); SetMovieSelection(theMovie, myStart, myDuration); myErr = MCMovieChanged(theMC, theMovie); return((OSErr)myErr) ;
We need to enable or disable the Set Preview to tion to Preview m e n u items, depending on w h e t h e r or preview. It's easy to determine w h e t h e r a movie simply call GetMovieSelection and check to see returned to us is greater than O, like this:
Selection and Set Seleca movie has a selection has a selection: we can w h e t h e r the duration
GetMovieSelection(myMovie, &myStart, &myDuration); myHasSelection = (myDuration > 0);
But it's a bit more complicated to determine w h e t h e r a movie has a movie preview. We need to check to see both w h e t h e r the movie has a nonzero movie preview duration and w h e t h e r any tracks in the movie are used in the movie preview. Listing 7.6 defines the QTInfo_MovieflasVreview function, which performs both of these checks. QTInfo_MovieHasVreview is very similar to OTInfo_MovieHasPoster (see Listing 7.3).
Listing 7.6 Determining whether a movie has a preview. Boolean QTInfo MovieHasPreview (Movie theMovie) m
TimeVal ue TimeVaI ue long long Track l ong Boolean
192
Chapter 7 The Informant
myStart; myDurat i on; myCount : OL; mylndex = OL; myTrack = NULL; myUsage = OL; myHasPreview = false;
/ / see i f the movie has a positive preview duration GetMoviePreviewTime(theMovie, &myStart, &myDuration) ; i f (myDuration > O) myHasPrevi ew = true; / / make sure that some track is used in the movie preview myCount = GetMovieTrackCount(theMovie) ; for (mylndex = 1; mylndex <= myCount; mylndex++) { myTrack = GetMovielndTrack(theMovie, mylndex) ; i f (myTrack == NULL) continue; myUsage = GetTrackUsage(myTrack) ; i f (myUsage & trackUsageInPreview) break; i f (myIndex > myCount) myHasPreview = false; return (myHasPrevi ew) ;
Playing Movie Previews The Movie Toolbox provides an easy way to show the user the exact contents of a movie preview. We can call the PlayMoviePreview function, like this: PlayMoviePreview(myMovie, NULL, OL);
When we execute PlayMoviePreview, the Movie Toolbox puts our movie into preview mode, plays the movie preview in the movie's graphics port, and then sets the movie back into normal playback mode. When the movie returns to normal playback mode, the current movie time is set to the end of the movie preview. The second parameter to PlayMoviePreview is a universal procedure pointer to a movie callout function, which the Movie Toolbox calls repeatedly while the preview is playing. We might use a movie callout function to provide a way for the user to stop the preview from playing (perhaps by checking the event queue for some particular key press). If we don't use a movie
Movie Previews 193
callout function, then the call to PlayMoviePreview is essentially synchronous: no other events will be processed until the movie preview finishes playing. The Movie Toolbox provides a way to play a movie preview without blocking other processing. We can call SetMoviePreviewMode with its second parameter set to true to put a particular movie into preview mode. 5etMoviePreviewMode restricts the active segment of the movie to the segment of the movie picked out by the preview's start time and duration; it also restricts the active tracks to those that have the trackUsageInPreview flag set in their track usage values. Once a movie has been set into preview mode, we can start it and stop it by calling the 5tartMovie and StopMovie functions. To exit movie preview mode, we can call SetMoviePreviewMode with its second parameter set to fa] se. (Note that QTInfo does not illustrate this method of playing movie previews; it calls P] ayMovieVrevi ew.I
Clearing Movie Previews Sometimes it's useful to clear a movie preview from a movie. We can do this by setting both the start time and duration of the movie preview to 0, like this"
SetMoviePreviewTime(theMovie, O, 0); Executing this line alone effectively prevents any movie preview from being displayed. But we also want to perform a few other actions. For one thing, we should remove any tracks from the movie that are used only in the movie preview. We can do this by examining the track usage value for each track in the movie and, if the usage value indicates that a track is used in the movie preview but not in the movie or the movie poster, calling Dispose~ MovieTrack to remove the track from the movie. Also, once we've removed any tracks that were used only in the movie preview, we should go back through the remaining tracks and reset their track usage values so that they can be used as part of a movie preview, if one is subsequently added. If we don't do this, the user might be unable to create a new movie preview, since it's possible that none of the remaining tracks in the movie has the trackUsageInPreview flag set in its track usage value. Listing 7.7 defines the QTInfo_ClearPreview function, which performs all three of these actions.
194
Chapter 7 The Informant
Listing 7.7 Clearing a movie preview. OSErr QTInfo ClearPreview (Movie theMovie, MovieController theMC) n
1ong long Track long ComponentResult
myCount mylndex myTrack myUsage myErr =
= OL; = OL; = NULL; = OL; noErr;
/ / set the movie preview start time and duration to 0 SetMoviePreviewTime(theMovie, O, 0); / / remove all tracks that are used *only* in the movie preview myCount = GetMovieTrackCount(theMovie) ; for (mylndex = myCount; mylndex >= 1; mylndex--) { myTrack = GetMovielndTrack(theMovie, mylndex) ; i f (myTrack == NULL) continue; myUsage = GetTrackUsage(myTrack) ; myUsage &= trackUsagelnMovie I trackUsagelnPreview I trackUsagelnPoster; i f (myUsage == trackUsagelnPreview) Di sposeMovieTrack (myTrack) ; / / add trackUsagelnPreview to any remaining tracks that are in the movie / / (so that subsequently setting the preview to a selection w i l l include / / these tracks) myCount = GetMovieTrackCount (theMovi e) ; for (mylndex = 1; mylndex <= myCount; mylndex++) { myTrack = GetMovielndTrack(theMovie, mylndex) ; i f (myTrack == NULL) continue; myUsage = GetTrackUsage(myTrack) ; i f (myUsage & trackUsagelnMovie) SetTrackUsage (myTrack, myUsage I trackUsagelnPrevi ew) ; myErr = MCMovieChanged(theMC, theMovie) ; return((OSErr)myErr) ;
Movie Previews 195
File P r e v i e w s Now consider this question: w h e n we call StandardGetFilePreview (or NavGetFile with the preview pane enabled), what is displayed in the preview section of the file-opening dialog box? Before you answer, take a look back at Figure 7.3. I suspect you're inclined to say that it's the movie preview. But before you make that your final answer, take a look at Figure 7.6, which shows another file-opening dialog box. And then take a look at Figure 7.7, which shows yet another file-opening dialog box. Thoroughly confused? I thought so.
196
Figure 7.6
A poster contained in a file-opening dialog box.
Figure 7.7
A description contained in a file-opening dialog box.
Chapter 7 The Informant
The correct a n s w e r to our little quiz is that the p r e v i e w displayed in a fileopening dialog box is w h a t ' s called a file preview, w h i c h is any information that gives the user an idea of w h a t ' s in the file. As w e ' v e seen, the file preview can be a movie poster or a movie p r e v i e w (if the file is a movie file)or any other data that describes or represents the file. On M a c i n t o s h computers, the default file p r e v i e w for a Q u i c k T i m e movie file is a m i n i a t u r e version of the movie poster frame, while on W i n d o w s c o m p u t e r s it's the first 10 seconds of the movie. But we are free to specify some other information as the file preview, if we so desire. Let's see h o w file p r e v i e w s are stored and created, to m a k e this all p e r h a p s a bit clearer.
Accessing File Previews On Macintosh computers, w h e n StandardGetFilePreview or NavGetFile needs to display a file p r e v i e w for a Q u i c k T i m e movie file, it first checks to see w h e t h e r the file is a double-fork or single-fork movie file. If the selected file is a double-fork movie file, StandardGetFi lePreview or NavGetFi le looks in the resource fork for a resource of type 'pnot'. The data in a 'pnot' resource is organized as a preview resource record, w h i c h is defined in ImageCompress i on. h like this: struct PreviewResourceRecord { unsigned long short OSType short
};
modDate; version; resType; resID;
The resType and resID fields specify the type and ID of some other resource, w h i c h contains the actual file p r e v i e w data or w h i c h itself indicates w h e r e to find that data. {Let's call that other resource the preview data resource.) For instance, if resType and resID pick out a resource of type 'PICT', then the picture in that resource will be used as the file preview [as in Figure 7.3). Similarly, if resType and resID pick out a resource of type 'TEXT', then the text in that resource will be used as the file preview [as in Figure 7.7). If resType and resID pick out a resource of type 'moor', then the movie preview start time and duration specified in that resource will be used to pick out the file preview [as in Figure 7.6). If there is no movie resource in the resource fork, then resID should be set to -1 (OxFFFF), w h i c h tells StandardGetFilePreview to use the movie previe w w h o s e start time and duration are stored in the movie atom in the file's data fork. In single-fork movie files, there is no resource fork. So StandardGetFi l ePreview opens the data fork and looks for an a t o m of type 'pnot', w h i c h it interprets in the same w a y as a 'pnot' resource, w i t h one small difference:
File Previews
197
the resID field is interpreted as a 1-based index of atom types in the movie file. For example, if the resIype field in the 'pnot' atom in a single-fork movie file is 'PICI' and the resID field is 1, then StandardGetFilePreview looks for the first atom in that file of type 'PICI', which it then uses as the file preview. There are a couple of "gotchas" here of which you should be aware. First, the NavGetFile function currently seems to work only with file previews specified by 'pnot' resources. If you're creating single-fork movie files (as I have recommended), don't expect them to have file previews in the fileopening dialog boxes displayed by NavGetFi l e. Worse yet, NavGetFile doesn't seem to know how to handle movie previews as file previews, even in double-fork movie files. Finally, StandardGetFilePreview doesn't seem to know how to handle movie previews as file previews w h e n stored in singlefork movies. (At least, I haven't been able to get them to work.) Our strategy in the following will be to create single-fork movie files with 'pnot' atoms in the format that is publicly documented. Then we'll just have to wait until StandardGetFilePreview and NavGetFile catch up to us (as I expect they will). You might be wondering (by the wayl w h y file preview resources and atoms have the type 'pnot'. The 'p' of course is for "preview"; but what's the 'not' all about? The constant assigned to the component that displays file previews is of no help in deciphering this: enum { ShowFilePreviewComponentType = FOURCHARCODE('pnot')
};
I'm told, by a knowledgeable source, that early development builds of the QuickTime software--prior to version 1.0--contained a preview component that wasn't very good. When the replacement was written, it was given the type 'pnot' as an abbreviation for "Preview? Not!"
Creating File Previews Ideally, we'd like the QuickTime movie files that we create to have file previews so that the user can get a reasonable idea of what's in those files w h e n they appear in the list of files in a file-opening dialog box. The Image Compression Manager provides the MakeFilePreview function, which we can use to create file previews. Apple documentation recommends calling MakeFilePreview whenever we save a movie file. So we can insert a call to MakeFilePreview in the two functions OTFrameUpdateMovieFile and QTFrame_SaveAsMovieFile (both in the file ComFramework.c) that handle the Save and Save As menu commands: m
198
Chapter7 The Informant
MakeFi I ePrevi ew(myRefNum, (I CMProgressProcRecordPtr) - 1) ; MakeFilePrevi ew sets the file preview for the file specified by the myRefNum parameter to be the current movie preview, if one exists; if the movie does not have a movie preview, then MakeFi ]ePreview creates a thumbnail version of the movie poster image and sets it to be the file preview. (A t h u m b n a i l is a small copy of an image, typically 80 pixels on the longer side.) If we want to create a file preview using some other type of data (for instance, text datal, we can call the ICM function AddFi l ePreview, which allows us to specify the type of data we want to use. But there is one big problem here: MakeFilePreview and AddFilePreview always add the file preview information to the movie file's resource fork. Indeed, MakeFilePreview and AddFilePreview will go so far as to create a resource fork for the movie file if it doesn't already have one so that they have a place to put the file preview they create. Needless to say, this behavior is going to w r e a k havoc with our desire to create only single-fork movie files. So, however tempting it might be to use MakeFilePreview to create our file previews, we're just going to have to resist that temptation. In short, QuickTime does not currently provide any API to add a file preview to a single-fork movie file. But based on w h a t we have learned about the way file previews are stored in single-fork files and, in the previous chapter, about the general structure of QuickTime movie files, it w o n ' t be too hard for us to do this ourselves. For, we know that a single-fork movie file is just a collection of atoms. And a file preview can be stored in a singlefork movie as an atom of type 'pnot' together with a preview data atom that holds the actual preview data. So all we really need to do is append an atom or two to a single-fork movie file. Figure 7.8 shows a single-fork movie file with no file preview (a) and that same file with a file preview (b). We'll
Y moor
Movie atom data
x mdat
Movie data
(a)
Y moov
Movie atom data
x mdat
Movie data
20 pnot
Preview resource record
z
PICT
Preview data
Figure 7.8 A single-fork movie file before and after adding a file preview.
File Previews
199
define a function QTInfo_MakeFilePreview that we can use to turn the top file into the bottom file. The function QTInfo MakeFilePreview is declared like this: m
OSErr QTInfo_MakeFilePreview (Movie theMovie, short theRefNum, ICMProgressProcRecordPtr theProgressProc) As you can see, QTInfo MakeFilePreview has the same parameters as MakeFilePreview, except that we also pass the movie identifier to QTInfo_MakeFilePreview. The second parameter to QTInfo_MakeFilePreview is the file reference number of the open movie file. If QTInfo_MakeFi lePreview is passed a reference to a resource fork, then it can just call MakeFilePreview to add the r e q u i r e d p r e v i e w resources to that resource fork, like this: m
i f (QTInfo_IsRefNumOfResourceFork(theRefNum)) { myErr = MakeFilePreview(theRefNum, theProgressProc) ; goto bai I ;
}
But if QTInfo_MakeFilePreview is passed the file reference number of a data fork, then we'll a s s u m e that w e m u s t add the file p r e v i e w i n f o r m a t i o n to the data fork. This involves adding a 'pnot' a t o m to the data fork, as well as a p r e v i e w data atom. Recall that an a t o m consists of an a t o m h e a d e r a n d some a t o m data. For a 'pnot' atom, the a t o m data is a record of type Previ ewResourceRecord. So we can construct the 'pnot' a t o m v e r y easily. First declare the local variables we need: Previ ewResourceRecord unsigned long
myPNOTRecord; myAtomHeader[2] ;
/ / an atom header
T h e n fill in the a t o m header: myAtomHeader[0] EndianU32 NtoB(sizeof(myAtomHeader) + sizeof(myPNOTRecord)) ; myAtomHeader[1] = EndianU32 NtoB(ShowFilePreviewComponentType);
Then fill in the a t o m data: GetDateTime(&myModDate) ; myPNOTRecord.modDate = EndianU32 NtoB(myModDate); myPNOTRecord.version = EndianS16 NtoB(O) ; myPNOTRecord.resType = EndianU32 NtoB(myPreviewType) ; myPNOTRecord.resID = EndianS16 NtoB(1) ; D
200
Chapter7 The Informant
All data in predefined QuickTime movie atoms must be in big-endian format, so here we use the macros EndianU32 NtoB and EndianS16 NtoB to convert from the computer's native-endian format into big-endian format. Notice that the resType field is set to myPreviewType. We'll create a file preview that is either a movie preview or a movie poster thumbnail, depending on w h e t h e r the movie has a movie preview: . .
m
i f (QTInfo MovieHasPreview(theMovie)) myPrevi ewType = MovieAID; else myPreviewType = kQTFileTypePicture;
The next thing we need to do is write the 'pnot' atom data onto the end of the movie file. We can use the File Manager functions GetEOF, SetEOF, SetFPos, and FSWrite to do this. Listing 7.8 (which we'll consider shortly) shows the exact steps involved in writing the data into the file. Now we need to write the actual preview data into an atom of the appropriate type. For a movie preview, we can just point to the 'moov' atom, w h i c h contains the start time and duration of the movie preview. For a file preview that contains a thumbnail of the movie poster frame, we need to retrieve the movie poster frame, create a thumbnail image from it, and write the atom onto the end of the movie file. We can call GetMoviePosterPict to get the movie poster image" myPicture = GetMoviePosterPict (theMovie) ; Then we can call the ICM function MakeThumbnaiIFromPicture to reduce the
poster image to a thumbnail image: myThumbnail = (PicHandle)NewHandleClear(4); myErr = MakeThumbnaiI FromPicture(myPi cture, O, myThumbnail , theProgressProc) ; If MakeThumbnail FromPi cture successfully creates the thumbnail image, we need to 611 in an atom header and write the header and thumbnail data into the movie file as an atom of type 'PICT', like this: myAtomHeader[O] = EndianU32 NtoB(sizeof(myAtomHeader) + GetHandl eSi ze ( (Handl e) myThumbnai I ) ) ; myAtomHeader[1] = EndianU32 NtoB(myPreviewType) ; D
/ / write the atom header into the f i l e mySize : sizeof(myAtomHeader) ; myErr = FSWrite(theRefNum, &mySize, myAtomHeader);
File Previews
2.,01
i f (myErr -= noErr) { / / write the atom data into the f i l e mySize = GetHandleSize((Handl e)myThumbnai I ) ; myErr = FSWrite(theRefNum, &mySize, *myThumbnail) ;
}
Listing 7.8 brings all of this together into a single function that writes the appropriate file preview into the resource fork or the data fork, depending on the kind of file reference number passed to it in the second parameter. Listing 7.8 Creating a file preview. OSErr QTInfo MakeFilePreview (Movie theMovie, short theRefNum, ICMProgressProcRecordPtr theProgressProc) unsigned long Previ ewResourceRecord long long unsigned long OSType OSErr
myModDate; myPNOTRecord; myEOF; mySi ze; myAtomHeader[2] ; myPrevi ewType; myErr = noErr;
/ / an atom header
i f (QTInfo IsRefNumOfResourceFork(theRefNum)) { myErr = MakeFilePreview(theRefNum, theProgressProc) ; goto bai I ;
}
/ / i f the movie has a movie preview, use that as the f i l e preview; otherwise use / / a thumbnail of the movie poster frame as the f i l e preview i f (QTInfo_MovieHasPreview(theMovie)) myPrevi ewType = MovieAID; else myPreviewType = kQTFileTypePicture; / / construct the 'pnot' atom; f i l l in the 'pnot' atom header myAtomHeader[O] = EndianU32 NtoB(sizeof(myAtomHeader) + sizeof(myPNOTRecord)) ; myAtomHeader[1] = EndianU32 NtoB(ShowFilePreviewComponentType) ; // fill
in the 'pnot' atom data
GetDateTime (&myModDate) ; myPNOTRecord.modDate = EndianU32 NtoB(myModDate); myPNOTRecord.version = EndianS16 NtoB(O); myPNOTRecord,resType = EndianU32 NtoB(myPreviewType) ; myPNOTRecord.resID = EndianS16 NtoB(1); m
m
202
Chapter 7 The Informant
/ / write the 'pnot' atom at the end of the data fork / / get the current logical e n d - o f - f i l e and extend i t by the desired amount myErr = GetEOF(theRefNum, &myEOF); i f (myErr ! = noErr) goto bai I ; myErr = SetEOF(theRefNum, myEOF + sizeof(myAtomHeader) + sizeof(myPNOTRecord)); i f (myErr ! = noErr) goto bai I ; / / set the f i l e mark myErr = SetFPos(theRefNum, fsFromStart, myEOF); i f (myErr ! = noErr) goto bai I ; / / write the atom header into the f i l e mySize = sizeof(myAtomHeader) ; myErr = FSWrite(theRefNum, &mySize, myAtomHeader); i f (myErr ! = noErr) goto bai I ; / / write the atom data into the f i l e mySize = sizeof(myPNOTRecord) ; myErr = FSWrite(theRefNum, &mySize, &myPNOTRecord); i f (myErr ! = noErr) goto bai I ; / / write the preview data atom at the end of the data fork i f (myPreviewType == MovieAID) { / / the 'pnot' atom refers to the existing 'moov' atom / / so no other preview data atom is required
}
i f (myPreviewType == kQTFileTypePicture) { PicHandle myPicture = NULL; PicHandle myThumbnail = NULL; / / get the poster frame picture myPicture = GetMoviePosterPict (theMovie) ; i f (myPicture ! = NULL) { / / create a thumbnail myThumbnail = (PicHandle)NewHandleClear(4) ; i f (myThumbnail != NULL) { myErr = MakeThumbnaiI FromPicture(myPicture, O, myThumbnail, theProgressProc) ;
File Previews 203
i f (myErr -= noErr) { myAtomHeader[O] = EndianU32_NtoB(sizeof(myAtomHeader) + GetHandl eSi ze ( (Handl e) myThumbnai I ) ) ; myAtomHeader[1] = EndianU32_NtoB(myPreviewType) ; / / write the atom header into the f i l e mySize = sizeof(myAtomHeader) ; myErr = FSWrite(theRefNum, &mySize, myAtomHeader); i f (myErr == noErr) { / / write the atom data into the f i l e mySize = GetHandleSize((Handl e)myThumbnai I ) ; myErr = FSWrite(theRefNum, &mySize, *myThumbnail);
}
Ki I l Picture (myThumbnai I ) ; Ki I l Picture (myPi cture) ;
bail" return (myErr) ;
}
I should point out that QTInfo_MakeFilePreview is not terribly smart about adding file previews to single-fork files. In particular, QTInfo MakeFilePreview doesn't bother to check whether the movie file already contains a 'pnot' atom. Instead, it simply appends a new 'pnot' atom and its associated preview data atom to the file. One consequence of this is that each time the user changes any aspect of the movie and saves it, a new thumbnail is appended to the movie file; but that thumbnail might never be used, since StandardGetFilePreview will always find the first 'pnot' atom and the first preview data atom. In the next chapter, we'll address this issue and see how to replace an existing 'pnot' atom and its associated preview data atom. w
Movie Annotations A QuickTime movie file can include one or more movie annotations, which provide descriptive information about the movie contained in the file. For example, movie annotations can indicate the names of the performers in the
21~1~ Chapter 7 The Informant
movie, the software that was used to create the movie, the names of the movie's writer and director, general information about the movie, and so forth. The header file Movies.h defines over two dozen kinds of movie annotations. For the present, we'll be concerned with only three of them, picked out by these constants: enum { kUserDataText Ful l Name kUserDataTextCopyri ght kUserDataText I nformat i on
}
= FOUR CHAR CODE(' 9 = FOUR CHAR CODE(' 9 = FOUR CHAR CODE(' 9
These are the three movie annotations that appear in the movie information dialog box displayed by the ShowMovieInformation function {see Figure 7.21. What we want to do now is show how to add these three kinds of movie annotations to a QuickTime movie file; or, if a movie file already contains annotations of these sorts, we w a n t to show how to edit those annotations. We'll handle both of these tasks by displaying an Edit Annotation dialog box that contains an editable text field in which the user can add or edit an annotation. For example, if the user selects Add Information in the Test m e n u of QTInfo but the frontmost movie has no information annotation, we'll display the dialog box shown in Figure 7.9. As you might have guessed from the constants we've listed, a movie annotation is stored in a QuickTime movie file as a piece of movie user data. We've already worked a little with the GetMovieUserData, GetUserDataltem, and SetUserDataItem functions for getting and setting a piece of a movie's
Figure 7.9 QTInfo's Edit Annotation dialog box.
Movie Annotations
205
user data (see Chapter 2, "Control"). Because the data for a movie annotation is always text data, here we'll use the GetUserDataText and AddUserDataText functions, which are specialized versions of GetUserDataItem and SetUserDataItem. W h e n the user selects one of our three m e n u items for adding or editing a movie annotation, QTInfo executes the QTInfo EditAnnotation function, passing it a movie identifier and the type of annotation to add or edit. For instance, if the user selects the Add Information item, QTInfo executes this block of code: m
case IDM ADD INFORMATION. mylsChanged = QTInfo EditAnnotation(myMovie, kUserDataTextlnformation) ; i f (mylsChanged) (**myWindowObject) . f l s D i r t y = true; mylsHandled = true; break; D
In the QTInfo_EditAnnotation function, we need to display the Edit Annotation dialog box, put the current movie annotation of the selected kind (if one exists) into the editable text field, allow the user to alter the annotation as desired, and then--if the user clicks the OK button--retrieve the new or edited annotation and attach it to the movie file as a piece of movie user data. Let's consider each of these steps.
Creating the Edit Annotation Dialog Box Our Edit Annotation dialog box contains four items, as shown in the ResEdit version of our dialog item list ('DITL') resource depicted in Figure 7.10. To be honest, I must admit that I simply "borrowed" this item list from the resource fork of the QuickTime Player application (and I was even too lazy to r e n u m b e r it). To refer to the items in this dialog box, we'll define these constants: #define #define #define #define #define
kEditTextResourceID kEdi tTextltemOK kEdi tTextltemCancel kEditTextltemEdi tBox kEdi tText ItemEdi tLabel
548 1
2 3 4
Our resource fork also contains a 'DLOG' resource of the same ID (again "borrowed" from QuickTime Player) that uses this dialog item list. We can open the Edit Annotation dialog box, therefore, by executing this code: myDialog : GetNewDialog(kEditTextResourcelD, NULL, (WindowPtr)-iL) ;
206
Chapter7 The Informant
Figure 7.10 The dialog item list for the Edit Annotation dialog box.
The dialog box is initially invisible, so that we have an opportunity to configure it before displaying it on the screen. For instance, we want to set both the default button {which is outlined in bold and activated w h e n the user types the Return or Enter key) and the Cancel button {which is activated w h e n the user types the Escape key or the Command-Period key combination). We can do this as follows: SetDialogDefaultltem(myDialog, kEditTextltemOK) ; SetDialogCancel Item(myDialog, kEditTextltemCancel) ;
Next, we want to set the static text movie annotation is being added or 'STR#' that contains three strings, one tion that QTInfo can handle. We'll strings: #define #define #define #define
kTextKi ndsResourceID kTextKindsFul IName kTextKi ndsCopyri ght kTextKi nds Informati on
item {item 4) to indicate which type of edited. I've added a resource of type for each of the types of movie annotause these constants to access those
2000 1
2 3
We'll simply retrieve one of these strings from that resource, according to the type of annotation that QTInfo_EditAnnotation is asked to handle, as shown in Listing 7.9.
Movie Annotations
207
Listing 7.9 Setting the label for a movie annotation. / / get a string for the specified annotation type switch (theType) { case kUserDataTextFul IName: GetlndString(myString, kTextKindsResourceID, kTextKindsFulIName) ; break; case kUserDataTextCopyright: GetlndString(myString, kTextKindsResourceID, kTextKindsCopyright); break; case kUserDataTextI nformati on: GetIndString(myString, kTextKindsResourceID, kTextKindslnformation); break; GetDialogltem(myDialog, kEditTextltemEditLabel, &myltemKind, &myltemHandle, &myltemRect); SetDialogItemText(myltemHandle, myString) ;
As you can see, we call GetDialogltem to get a handle to the static text item and SetDialogItemText to set the string as the text of that item.
Showing the Current Annotation We also want to call SetDialogltemText to set the current annotation, if one exists, as the text of the editable text item. First, however, we need to find the current annotation of the specified type. As m e n t i o n e d earlier, we'll use the GetUserDataText function to do this. GetUserDataText reads the movie annotation of a specified type from a user data item list, w h i c h we first obtain by calling GetMovieUserData, like this: myUserData = GetMovieUserData (theMovi e) ; GetUserDataText returns the requested information in a handle, w h i c h it resizes as necessary to exactly hold the text. So we can retrieve the current movie annotation of the desired type using code like this: myHandle = NewHandleClear(4) ; i f (myHandle I= NULL) { myErr = GetUserDataText(myUserData, myHandle, theType, 1, GetScri ptManagerVari abI e ( smRegi onCode)) ; / / some I ines omitted here
}
208
Chapter 7 The Informant
The final parameter passed to GetUserDataText is a region code, which specifies a version of a written language of a particular region in the world. It's possible to have several movie annotations of the same type, which differ only in their region code--that is to say, their language. Here we're using the Script Manager function GetScriptManagerVariable to get the region code associated with the user's current script system. Once we've called GetUserDataText to get the current annotation of the specified type, we need to copy the text in myHandle into a Pascal string. That's because SetDialogItemText takes a Pascal string as a parameter, not a handle. We can use the function QTInfo TextHandleToPString, defined in Listing 7.10, to make this conversion.
Listing
7.10
Copying text from a handle into a Pascal string.
void QTInfo_TextHandleToPString (Handle theHandle, Str255 theString)
{
short
myCount;
myCount = GetHandleSize(theHandle) ; i f (myCount > 255) myCount = 255; theStri ng[O] = myCount; BlockMoveData(*theHandle, &(theString[1]), myCount) ;
So now we are finally ready to insert the existing annotation into the Edit Annotation dialog box. We can do this with these two lines of code" GetDialogltem(myDialog, kEditTextltemEditBox, &myltemKind, &myltemHandl e, &myltemRect) ; SetDialogltemText (myltemHandle, myString) ;
The final thing we need to do before displaying the dialog box to the user is set the current selection range of the annotation text. When QuickTime Player displays its Edit Annotation dialog box, it selects all the text in the editable text item. We'll follow this example by calling Sel ectDi al ogI temText like this: SelectDialogltemText(myDialog, kEditTextltemEditBox, O, myString[O]) ;
At this point, the Edit Annotation dialog box is fully configured. Its static text item has been updated to indicate which type of movie annotation is being edited, and the current annotation of that type has been inserted into the editable text item. We can finish up by actually showing the dialog box to the user: Movie Annotations
209
MacShowWindow(GetDi al ogWindow(myDi al og) ) ;
Retrieving the Edited Annotation We allow the user to interact with the items in the Edit Annotation dialog box by calling Modal Di a 1og" do { ModaIDialog(gModal Fi IterUPP, &myltem) ; } while ((myItem != kEditTextltemOK) && (myltem ! = kEditTextltemCancel));
As you can see, Modal Dial og is called continually until the user clicks the OK or Cancel button (or types a key or key combination that is i n t e r p r e t e d as a click on one of those buttons). If t h e user clicks the Cancel button, we should just exit the QTInfo_EditAnnotation function after disposing of the Edit Annotation dialog box and p e r f o r m i n g any other necessary cleanup. i f (myltem ! = kEditTextltemOK) goto bai I ;
But if the user clicks the OK button, we need to retrieve the text in the editable text item and set it as the movie annotation of the specified type. We can get the edited text like this" GetDi aIog Item (myDi aI og, kEdit Text I temEdi tBox, &myI temKi nd, &myI temHandl e, &myI temRect) ; GetDialogltemText(myltemHandle, myString) ;
We want to call AddUserDataText to insert the user's edited annotation into the movie user data list. To do this, we first need to convert the Pascal string returned by GetDialogItemText into a handle. We can use the QTInfo_PStringToTextHandl e function, defined in Listing 7.11, to handle this conversion.
Listing 7.11 Copying text from
a Pascal string into a handle.
void QTInfo_PStringToTextHandle (Str255 theString, Handle theHandle)
{
SetHandleSize(theHandle, theString[O]) ; i f (GetHandleSize(theHandle) ! = theString[O]) return; BlockMoveData(&(theString[1]), *theHandle, theString[O]) ;
210
Chapter 7 Thelnformant
Now we are ready to call AddUserDataText" myErr = AddUserDataText(myUserData, myHandle, theType, 1, GetScri ptManagerVari abI e ( smRegionCode) ) ;
Again, we're calling GetScriptManagerVariable to get the user's current region code so that the annotation is written into the movie file in a form recognizable to the user. Listing 7.12 shows the complete function QTInfo_ Edi tAnnotati on.
Listing
7.12
Editing a movie annotation.
Boolean QTInfo_EditAnnotation (Movie theMovie, OSType theType) DialogPtr short short GrafPtr Handle short Handle UserData Rect Str255 Boolean OSErr
myDialog = NULL; myltem; mySavedResFi I e; mySavedPort; myHandle = NULL; myltemKi nd; myltemHandl e; myUserData= NULL; myltemRect; myString; mylsChanged = false; myErr = noErr;
/ / save the current resource f i l e and graphics port mySavedResFi le = CurResFile() ; Get Port (&mySavedPort) ; / / set the application's resource f i l e UseResFi I e (gAppResFi le) ; / / get the movie user data myUserData = GetMovieUserData (theMovi e) ; i f (myUserData == NULL) goto bai I ; / / create the dialog box in which the user will add or edit the annotation myDialog = GetNewDialog(kEditTextResourceID, NULL, (WindowPtr)-lL) ; i f (myDialog == NULL) goto bai I ;
Movie Annotations 211
# i f TARGET API MAC CARBON SetPortDi al ogPort (myDial og) ; #else MacSetPort (myDial og) ; #endif SetDialogDefaultltem(myDialog, kEditTextltemOK) ; SetDialogCancel Item(myDialog, kEditTextItemCancel) ; / / get a string for the specified annotation type switch (theType) { case kUserDataTextFul IName" GetIndString(myString, kTextKindsResourceID, kTextKindsFulIName) ; break; case kUserDataTextCopyri ght 9 GetlndString(myString, kTextKindsResourceID, kTextKindsCopyright); break; case kUserDataTextInformati on 9 GetlndString(myString, kTextKindsResourceID, kTextKindslnformation); break; GetDialogltem(myDialog, kEditTextltemEditLabel, &myltemKind, &myltemHandle, &myltemRect); SetDialogltemText(myltemHandle, myString) ; / / set the current annotation of the specified type, i f i t exists myHandle = NewHandleClear(4) ; i f (myHandle ! = NULL) { myErr = GetUserDataText(myUserData, myHandle, theType, 1, GetScri ptManagerVari able (smRegi onCode)) ; i f (myErr == noErr) { QTInfo_TextHandleToPString(myHandle, myString) ; GetDi al ogltem(myDi al og, kEditText ItemEdi tBox, &myltemKind, &myltemHandle, &myltemRect); SetDialogltemText(myltemHandle, myString) ; SelectDialogltemText(myDialog, kEditTextltemEditBox, O, myString[Ol) ; Di sposeHandl e (myHandle) ; MacShowWindow(GetDi al ogWindow(myDial og) ) ;
212
Chapter7 Thelnformant
/ / display and handle events in the dialog box until the user clicks OK or Cancel do { ModaIDialog(gModal Fi IterUPP, &myltem) ; } while ((myltem ! = kEditTextltemOK) && (myltem ! = kEditTextltemCancel)); / / handle the selected button i f (myItem ! = kEditTextltemOK) goto bai I ; / / retrieve the edited text myHandle = NewHandleClear(4) ; i f (myHandle ! = NULL) { GetDi al ogltem(myDi al og, kEditText ItemEdi tBox, &myltemKind, &myltemHandle, &myltemRect); GetDi al ogl temText (myI temHandl e, myStri ng); QTInfo_PStringToTextHandle(myString, myHandle) ; myErr = AddUserDataText(myUserData, myHandle, theType, 1, GetScri ptManagerVari abI e ( smRegionCode) ) ; mylsChanged = (myErr == noErr) ; Di sposeHandl e (myHandle) ; bail 9 / / restore the previous resource f i l e and graphics port MacSetPort (mySavedPort) ; UseResFi I e (mySavedResFi I e) ; i f (myDialog ! = NULL) Di sposeDi al og (myDial og) ; return (myI sChanged) ;
Note that QTlnfo EditAnnotation returns a Boolean value that indicates whether the user clicked the OK button and the specified movie annotation was successfully updated. QTInfo uses that value to determine w h e t h e r it should mark the movie as dirty [and hence in need of saving). It's possible, however, that the user clicked the OK button without having altered the movie annotation in the editable text item. In that case, the movie would be marked as dirty even though its user data had not actually changed. It would be easy to modify QTInfo EditAnnotation so that it compares the original annotation and the annotation later retrieved from the text box to see whether they differ. This enhancement is left as an exercise for the reader. u
Movie Annotations 213
(It's worth noting, however, that the behavior of QTInfo in this regard is identical to that of QuickTime Player.)
Conclusion In this chapter, we've learned how to get and set some of the information that's stored in a QuickTime movie file. We've seen how to work with movie posters and movie previews, and we've seen how to add file previews to both double-fork and single-fork QuickTime movie files. We still have a little bit of work to do on the QTlnfo_MakeFilePreviewfunction (which we've deferred until the following chapter), but already it can write file previews into single-fork movie files. We've also seen how to add annotations to a movie file and edit a file's existing annotations. Our sample application QTInfo allows the user to edit any of the three kinds of annotations displayed in the movie information dialog box. With just a little bit of work, however, the QTInfo_EditAnnotation function could be modified to support editing any type of movie annotation. So what we've got are the essential elements of a general-purpose tool for adding and changing any text-based movie user data. But, as I've said, we still have some work to do to clean up one or two loose ends in this chapter's code. In the next chapter, we'll see how to find specific atoms in a QuickTime movie file. We'll also discover another kind of atom structure that can be found lurking deep inside QuickTime movie files.
214
Chapter7 The Informant
The Atomic Caf6
Working with Atoms and Atom Containers
Introduction In the past two chapters, we've been concerned at least in part with atoms, the basic building blocks of QuickTime movie files. Atoms are utterly simple in structure (a 4-byte length field, followed by a 4-byte type field, followed by some data), and this utter simplicity means that atoms can be used for a very wide range of tasks. Indeed, the atom-based structure used by QuickTime movie files is so general and so flexible that it has been adopted by the International Standards Organization (ISO) as the basis for the development of a unified digital media storage format for the MPEG-4 specification. In this chapter, we're going to continue investigating the basic structure of QuickTime files as sequences of atoms. You might recall that in the previous chapter, we left some unfinished business lying around. Namely, we need to see how to replace an existing atom of a particular type instead of just adding a new atom of that type. It turns out that handling this task in the general case is reasonably difficult, since we can't safely move the movie data atom around in a movie file without doing a lot of work. For the present, we'll be content to see how to amend the OTlnfo_MakeFilePreview function we developed last time so that there is at most one 'pnot' atom in a QuickTime movie file. Because an atom can contain any kind of data whatsoever, it can contain data that consists of one or more other atoms. So, atoms can be arranged hierarchically. We'll take a few m o m e n t s to consider the hierarchical arrangement of a movie atom (the main repository of bookkeeping information in a QuickTime movie file). Then we'll show how to put our atomfusing powers to work to create a shortcut movie file, which is a QuickTime movie file that does nothing more than point to some other QuickTime movie file. Once we've played with atoms for a while, we're going to shift gears rather abruptly to consider a second kind of atom-based structure, which
215
we'll call an atom container. Atom containers are structures of type QTAtomContai ner that are often used inside of QuickTime movie data atoms to store various kinds of information (for example, media samples). They were developed primarily to address some of the shortcomings of atoms. In particular, the Movie Toolbox provides an extensive API for working with atom containers; among other things, this API makes it easy to create and access data arranged hierarchically within atom containers. We'll get some hands-on experience with atom containers in two ways. First, we'll see how to get and set the user's Internet connection speed preference, which is a piece of information that QuickTime stores internally and happily gives, in the form of an atom container, to anyone w h o asks. Second, we'll see how to add a movie track to a QuickTime movie. By using movie tracks, we can embed entire QuickTime movies inside of other QuickTime movies. The movie media handler, which manages movie tracks, is one of the most exciting new features in QuickTime 4.1. Once we understand h o w to work with atom containers, it'll be easy to add movie tracks to an existing QuickTime movie. Before we begin, though, a word about terminology. As you've been warned, this chapter is going to discuss two different ways of organizing data, both of which are (for better or worse) called "atoms." The first kind of atom is the one that's used to define the basic structure of QuickTime movie files. The second kind of atom is the one that was introduced in QuickTime 2.1 for storing data in some kinds of media samples (and for other tasks as well); these kinds of atoms are structures of type QTAtom that are stored inside of an atom container. Some of Apple's QuickTime documentation refers to the first kind of atom as a classic atom (perhaps in the same spirit that one refers to a classic car: it's been around a while) and to the second kind of atom as a Q T atom (drawing of course on the data type QTAtom). Some other documentation refers to the first kind of atom as a chunk atom (perhaps because it's just a chunk of data?). I'm not particularly happy with any of these terms, so I'm going to refer to the first kind of atom simply as an atom and to the second kind as an atom container atom. In other words, an atom container atom (of type QTAtom) is always found inside of an atom container (of type QTAtomContainer). Generally, here and in the future, the context will make it clear which kind of atom we're considering, so we can usually get by just talking about atoms.
File Previews: The Sequel In the previous chapter, we saw how to add a 'pnot' atom to a QuickTime movie file, to create a single-fork movie file with a file preview. (A file preview is the movieclip, image, text, or other data that appears in the preview
216
Chapter8 The Atomic Caf~
pane of the file-opening dialog box displayed by a call to StandardGetFilePreview or NavGetFi ]e.) Our strategy was simple: each time the user saves a movie, add a preview atom and [if necessary] a preview data atom to the QuickTime movie file. But we recognized that ultimately we would need to refine this strategy to avoid ending up with multiple preview atoms and preview data atoms. It's time to make some changes to our QTInfo application. In this section, we'll see how to upgrade QTInfo into a nearly identical application, called QTInfoPlus, that handles file preview atoms better.
Removing Existing Previews In fact, we can solve this little problem by adding a single line of code to the OTInfo MakeFi]ePreview function. Immediately after determining that the file reference number passed to OTInfo_MakeFi lePreview picks out a data file, we can execute this code" n
QTInfo RemoveAll PreviewsFromFi le(theRefNum) ; The QTInfo_RemoveAlIPreviewsFromFile function looks through the specified
open data file and removes any existing preview atoms (that is, atoms of type 'pnot') from that file. In addition, this function removes any preview data atoms referenced by those preview atoms, unless the preview data atoms are of type 'moor'. {We don't want to remove atoms of type 'moov', of course, since they contain essential information about the movie.)QTInfo_RemoveAl 1PreviewsFromFile is defined in Listing 8.1.
Listing 8.1 Removing all preview atoms from a QuickTime movie file. OSErr QTInfo RemoveAll PreviewsFromFile (short theRefNum)
{
long short short OSErr
myAtomType = OL; myAtomlndex = O; myCount = O; myErr = noErr;
/ / count the preview atoms in the f i l e myCount = QTInfo_CountAtomsOfTypelnFile(theRefNum, OL, ShowFilePreviewComponentType) ; while (myCount > O) { / / get the preview data atom targeted by this preview atom myAtomType = ShowFiI ePrevi ewComponentType; myAtomlndex = myCount; myErr = QTInfo FindPreviewAtomTarget(theRefNum, &myAtomType, &myAtomlndex) ; / / i f the preview data atom is the last atom in the f i l e , remove i t / / (unless i t ' s a 'moov' atom)
File Previews: The Sequel 2.17
i f (myErr == noErr) i f (myAtomType != MovieAID) i f (QTInfo IsLastAtomlnFile(theRefNum, myAtomType, myAtomlndex)) QTInfo_RemoveAtomFromFile(theRefNum, myAtomType, myAtomlndex); m
/ / remove or free the preview atom i f (QTInfo IsLastAtomlnFile(theRefNum, ShowFilePreviewComponentType, myCount)) QTInfo_RemoveAtomFromFi I e (theRefNum, ShowFiI ePrevi ewComponentType, myCount); else QTI n fo_FreeAtomInFi I e (theRefNum, ShowFi I ePrevi ewComponentType, myCount); / / i f the preview data atom s t i l l exists, remove or free i t i f (myErr == noErr) i f (myAtomType l= MovieAID) i f (QTInfo IsLastAtomlnFile(theRefNum, myAtomType, myAtomlndex)) QTInfo RemoveAtomFromFile(theRefNum, myAtomType, myAtomlndex); else QTInfo FreeAtomInFile(theRefNum, myAtomType, myAtomlndex); myCount--; return (myErr) ;
As you can see, QTlnfo_RemoveAl 1PreviewsFromFi le calls a handful of other functions defined by our application. These other functions do things like count the number of existing preview atoms, find the preview data atom that is the target of a preview atom, determine whether a given atom is the last atom in the file, and so forth. OTlnfo_RemoveAl 1PreviewsFromFi le puts these functions to work like this: for each preview atom in the file (starting with the one nearest the end of the file), find the preview data atom that is referenced by the preview atom. If that target atom isn't a movie atom and it's the last atom in the file, remove it from the file. Then, if the preview atom is the last atom in the file, remove it as well. If the preview atom isn't the last atom in the file, then change it into a free a t o m (that is, an atom whose type is FreeAtomType). By changing the atom type, we're converting the preview atom into a block of unused space at its current location in the movie file. QuickTime simply ignores any atoms of type FreeAtomType that it encounters w h e n reading through a movie file. You might think that we could just remove a preview atom from the file and, if it isn't the last atom in the file, move any following atoms up in the
218
Chapter8 The Atomic Caf~
file. This would avoid creating "islands" of unused space in our file, but it would be a dangerous thing to do. That's because some atoms in a QuickTime file reference data in other atoms by storing offsets from the beginning of the file. In general, we want to avoid moving atoms around in a QuickTime movie file. It's safer just to convert any u n w a n t e d atoms that are not at the end of the file into free atoms. Once we've removed a preview atom (if it's the last atom in the file} or converted it into a free atom (if it isn't), we then look once again to see if the preview data atom is the last item in the file. This might happen if the preview data atom originally preceded the preview atom and the preview atom was the last atom in the file. If the preview data atom is now the last atom in the file, it's removed; otherwise, it's converted into a free atom. The net result of all this is to remove any existing preview and preview data atoms from the file, either by truncating the file to exclude those atoms or by converting them into free atoms. At this point, OTInfo_MakeFilePreview can safely add a new preview atom and (if necessary) a preview data atom to that file. So, w h e n all is said and done, the QuickTime movie file will end up with exactly one preview atom and one preview data atom. In the next few subsections, we'll consider how to define the various QTInfoPlus functions called by QTInfo_RemoveAl l Previ ewsFromFi I e.
Finding and Counting Atoms The most fundamental thing we need to be able to do, w h e n working with a file that's composed of atoms, is find an atom of a specific type and index in that file. For instance, we might need to find the first movie atom, or the third preview atom, or the third 'PICT' atom in the file. So we want to devise a function that takes an atom type and an index and then returns to us the position in the file at which that atom begins, if there is an atom of that type and index in the file. Otherwise, the function should return some error. This task is reasonably straightforward. All we need to do is start at the beginning of the file (or at some other offset in the file specified by the caller) and inspect the type of the atom at that location. If the desired index is 1 and the desired atom type is the type of that atom, we're done" we've found the desired atom. Otherwise, we need to keep looking. We can find the next atom in the file by moving forward in the file by the size of the atom currently under consideration. We continue inspecting each atom and moving forward in the file until we find the atom of the specified type and index or until we reach the end of the file. Listing 8.2 defines the function QTInfo_FindAtomOfTypeAndlndexlnFi le, which is our basic atom-finding tool.
File Previews: The Sequel 2119
Listing 8.2
Finding an atom in a QuickTime movie file.
OSErr QTInfo_FindAtomOfTypeAndlndexlnFile (short theRefNum, long *theOffset, long theAtomType, short thelndex, long *theDataSize, Ptr *theDataPtr) short l ong long long long OSType Ptr Boolean OSErr
mylndex = 1; myFi I eSi ze; myFi I ePos = OL; myAtomHeaderr2]; mySize = OL; myType = OL; myDataPtr = NULL; isAtomFound= false; myErr = paramErr;
i f (theOffset == NULL) goto bai I ; i f (QTInfo IsRefNumOfResourceFork(theRefNum)) goto bai I ; m
myFilePos = *theOffset; / / get the total size of the f i l e GetEOF(theRefNum, &myFileSize) ; while (!isAtomFound) { myErr = SetFPos(theRefNum, fsFromStart, myFilePos) ; i f (myErr ! = noErr) goto bai I ; / / read the atom header at the current f i l e position mySize = sizeof(myAtomHeader) ; myErr = FSRead(theRefNum, &mySize, myAtomHeader); i f (myErr i= noErr) goto bai I ; mySize = EndianU32 BtoN(myAtomHeader[O]); myType = EndianU32 BtoN(myAtomHeader[1]); i f ((mylndex =: thelndex) && ((theAtomType : : myType) II (theAtomType : : kQTlnfoAnyAtomType))) {
220
Chapter 8 The Atomic Cafe
/ / we found an atom of the specified type and index; / / return the atom i f the c a l l e r wants i t i f (theDataPtr ! = NULL) { myDataPtr = NewPtrClear(mySize) ; i f (myDataPtr == NULL) { myErr = MemError() ; goto bai I ;
}
/ / back up to the beginning of the atom myErr = SetFPos(theRefNum, fsFromStart, myFilePos) ; i f (myErr ! = noErr) goto bai I ; myErr = FSRead(theRefNum, &mySize, myDataPtr) ; i f (myErr ! = noErr) goto bai I ;
i sAtomFound = true; } else { / / we haven't found an atom of the specified type and index; keep on looking myFi I ePos += mySize; i f ((theAtomType == myType) II (theAtomType == kQTInfoAnyAtomType)) myIndex++; / / make sure we're moving forward in the f i l e , but not too f a r . . . i f ((mySize <= O) II (myFilePos > (myFileSize - sizeof(myAtomHeader)))) {
} }
}
myErr = cannotFindAtomErr; goto bai I ;
/ / while (!isAtomFound)
/ / i f we got to here, we found the correct atom i f (theOffset l= NULL) *theOffset = myFilePos; i f (theDataPtr ! = NULL) *theDataPtr = myDataPtr; i f (theDataSize l= NULL) *theDataSize = mySize; bail: i f (myErr l : noErr)
File Previews: The Sequel 221
i f (myDataPtr != NULL) Di sposePtr(myDataPtr) ; return (myErr) ;
QTInfo FindAtomOfTypeAndlndexlnFile returns to its caller the offset within the fl]e of the beginning os the atom of the desired type and index. In addition, if the caller passes in non-NULL values in the theDataPtr or theDataSize parameters, QTInfo_FindAtomOfTypeAndlndexInFile returns a copy os the entire atom (including the atom header) or the atom size to the caller. The returned offset, data, and atom size can be used for a variety of purposes. For instance, Listing 8.3 defines the QTInfoCountAtomsOfTypeInFile function, which counts the number of atoms of a specific type in a file.
Listing 8.3
Counting the atoms in a QuickTime movie file.
short QTInfo_CountAtomsOfTypelnFile (short theRefNum, long theOffset, long theAtomType)
(
short long long OSErr
mylndex = O; myFilePos = theOffset; myAtomSize = OL; myErr = noErr;
i f (QTInfo IsRefNumOfResourceFork(theRefNum)) return (mylndex) ; while (myErr == noErr) { myErr = QTInfo FindAtomOfTypeAndlndexlnFile(theRefNum, &myFilePos, theAtomType, 1, &myAtomSize, NULL); i f (myErr == noErr) mylndex++; myFilePos += myAtomSize; m
/ / avoid an infinite loop... i f (myAtomSize <= O) break; return (mylndex) ;
222.
Chapter 8 The Atomic Caf~
QTInfo_CountAtomsOfTypelnFi l e uses the offset and atom size returned by QTInfo_FindAtomOfTypeAndIndexInFile to walk through the file looking for atoms of the specified type. Similarly, it's easy to use QTInfo_FindAtomOfTypeAndIndexInFile to determine whether a particular atom is the last atom in a file. We simply call QTInfo_FindAtomOfTypeAndIndexInFile to get the offset in the file of the given atom and then call it again to see if there are any atoms of any kind following that atom. Listing 8.4 defines a function that does precisely this. Listing 8.4
Determining whether an atom is the last atom in a file.
Boolean QTInfo IsLastAtomlnFile (short theRefNum, long theAtomType, short thelndex)
{
Boolean long long OSErr
isLastAtom = false; myOffset = OL; myAtomSize = OL; myErr = noErr;
/ / find the offset and size of the atom of the specified type and index in the f i l e myErr = QTInfo FindAtomOfTypeAndlndexlnFile(theRefNum, &myOffset, theAtomType, thelndex, &myAtomSize, NULL); i f (myErr == noErr) { / / look for an atom of any type following that atom myOffset += myAtomSize; myErr = QTInfo FindAtomOfTypeAndIndexlnFile(theRefNum, &myOffset, kQTInfoAnyAtomType, 1, NULL, NULL); i f (myErr ! = noErr) isLastAtom = true; m
return(i sLastAtom) ;
Finding the P r e v i e w Data A t o m Given a preview atom, we sometimes need to know which other atom is the target of that atom. In other words, we want to find the atom that we've been calling the preview data atom--the atom that contains the data for the file preview. This is fairly easy" we just need to read the data in the preview atom, which has the structure of a PreviewResourceRecord record. Listing 8.5 defines the QTInfo FindPreviewAtomTarget function, which does this.
File Previews' The Sequel
223
Listing 8.5 Findingthe target of a preview atom. OSErr QTInfo FindPreviewAtomTarget (short theRefNum, long *theAtomType, short *theIndex)
{
long Previ ewResourceRecord long OSErr
myOffset = OL; myPNOTRecord; mySi ze; myErr = noErr;
i f ((theAtomType == NULL) II (thelndex == NULL)) return (paramErr) ; / / find the offset of the atom of the specified type and index in the f i l e myErr = QTInfo_FindAtomOfTypeAndlndexlnFile(theRefNum, &myOffset, *theAtomType, *thelndex, NULL, NULL); i f (myErr == noErr) { / / set the f i l e mark to the beginning of the atom data myErr = SetFPos(theRefNum, fsFromStart, myOffset + (2 * sizeof(long))) ; i f (myErr == noErr) { / / read the atom data mySize = sizeof(myPNOTRecord) ; myErr = FSRead(theRefNum, &mySize, &myPNOTRecord); i f (myErr == noErr) ( *theAtomType = EndianU32 BtoN(myPNOTRecord.resType) ; *thelndex = EndianS16 BtoN(myPNOTRecord.resID) ;
}
,
B
return (myErr) ;
QTlnfo FindPreviewAtomTarget calls QTlnfo FindAtomOfTypeAndlndexlnFile to find the location of the atom whose type and index are passed to it in the theAtomType and theIndex parameters. Then it advances the file mark to the beginning of the atom data and reads the atom data into myPNOTRecord. The type and index of the preview data atom (suitably converted from big-endian to native-endian form) are then returned to the caller. m
Removing and Freeing Atoms It's quite easy to remove an atom from a file or convert it into a free atom. Listing 8.6 shows how we define the QTInfo_RemoveAtomFromFile function, which removes an atom from a file by truncating the file at the beginning of the atom {by calling SetEOF). Note that any atoms that follow the specified
224
Chapter 8 The Atomic Caf~
atom are also r e m o v e d from the file. To avoid any problems, we s h o u l d always call QTInfo IsLastAtomInFile to m a k e sure that the atom to be r e m o v e d from the file is the last atom in the file.
Listing
8.6
Removing an atom from a file.
OSErr QTInfo RemoveAtomFromFile (short theRefNum, long theAtomType, short thelndex)
{
long long OSErr
myOffset -- OL; myAtomSize = OL; myErr = noErr;
/ / find the offset of the atom of the specified type and index in the f i l e myErr = QTInfo FindAtomOfTypeAndlndexlnFile(theRefNum, &myOffset, theAtomType, thelndex, &myAtomSize, NULL); i f (myErr == noErr) myErr = SetEOF(theRefNum, myOffset); return (myErr) ;
It's almost as easy to convert an atom into a free atom. All we need to do is position the file m a r k to the beginning of the type field in the atom h e a d e r and write the value 'free' into that field. Listing 8.7 shows how we do this.
Listing
8.7
Freeing an atom in a file.
OSErr QTInfo FreeAtomlnFile (short theRefNum, long theAtomType, short thelndex)
{
m
OSType long long OSErr
myType = EndianU32 NtoB(FreeAtomType); mySize = sizeof(myType) ; myOffset = OL; myErr = noErr;
/ / find the offset of the atom of the specified type and index in the f i l e myErr = QTInfo FindAtomOfTypeAndlndexlnFile(theRefNum, &myOffset, theAtomType, thelndex, NULL, NULL); i f (myErr =- noErr) { / / change the atom type to 'free' myErr = SetFPos(theRefNum, fsFromStart, myOffset + sizeof(long)); i f (myErr --= noErr) myErr = FSWrite(theRefNum, &mySize, &myType);
}
return (myErr) ;
File Previews: The Sequel
225
So allow from forth.
far, then, w e ' v e m a n a g e d to define a handful of utility functions that us to find atoms in files, get the sizes of those atoms, r e m o v e a t o m s files, count the n u m b e r of atoms of a specific type in a file, and so These are precisely the functions called by the QTInfo_RemoveAllPreviewsFromFile function, w h i c h w e ' r e using to m a k e sure that any QuickTime movie file we create or edit has at most one p r e v i e w resource. It w o u l d be easy (and fun) to define an entire library of a t o m utilities, but we'll have to restrain our p r o g r a m m i n g urges here. We've got other w o r k to do.
S h o r t c u t M o v i e Files I m e n t i o n e d earlier that an atom can contain any kind of data, and in particular it can contain other atoms. That is to say, atoms can be arranged hierarchically. Up to now, w e ' v e w o r k e d w i t h a Q u i c k T i m e movie file only as a m e r e concatenation of atoms. We've looked at the data of several of those atoms, but we h a v e n ' t yet met any atoms that contain other atoms. It's time for that to change. A good example of an atom that contains other atoms is the movie a t o m itself. A typical movie atom contains a track atom (of type TrackAID) for each track in the Q u i c k T i m e movie, along with other atoms that contain the movie metadata. A movie atom can also contain a movie user data atom (of type UserDataAID), w h i c h contains the movie user data. A track atom, in turn, contains other atoms that define the track characteristics and that point to the media data, and so on, as deep as is necessary to completely characterize a movie and its data. An atom that contains no other atoms is called a leaf atom. A leaf a t o m m a y or m a y not actually contain any data. Typically, a leaf atom does contain data, but it's possible that the very p r e s e n c e in the file of the atom has significance. In that case, the leaf atom consists solely of the 8-byte a t o m header. An atom that contains one or more other atoms is called a container atom. A movie atom is a container atom. By contrast, a preview atom is a leaf atom, since it contains data but no other atoms. (A preview atom points to or references another atom, but it does not contain it.) Let's build a container atom. Now, it's beyond our c u r r e n t capabilities to build a typical movie atom, with all its complicated s u b a t o m s and subsubatoms. But there is a kind of Q u i c k T i m e movie file that consists entirely of a movie atom and that is simple enough for us to build; this kind of file is called a shortcut movie file. A shortcut movie file is a movie file that picks out a single other movie file. It's rather like an alias file in the Macintosh file system or a shortcut on Windows. Opening a shortcut movie file using the
226
Chapter8 The Atomic Caf~
Movie Toolbox function 0penNovieFi l e causes QuickTime to look for the file that is referred to by the shortcut movie file; if that target file can be found, then 0penMovieFi ]e opens it and returns to the caller a file reference n u m b e r for that target file. Shortcut movie files provide a cross-platform mechanism for referring to QuickTime movie files. They can be useful in all the same ways that alias files (on Mac) or shortcuts (on Windows) can be useful, at least w h e n working with QuickTime movies. For instance, a Web page might contain an embedded URL to a shortcut movie file. To update the movie displayed in the Web page, the webmaster needs only to create a new shortcut movie file that refers to the updated movie and then put the new shortcut movie file in the location occupied by the previous shortcut movie file. In this way, the contents of the Web page can be changed without altering the actual HTML tags of the page. QuickTime has supported shortcut movie files since version 3.0. QuickTime version 4.0 introduced the Movie Toolbox function CreateShortcutMovieFile, which we can use to create a shortcut movie file. But the structure of shortcut movie files is so simple that we can build them ourselves. A shortcut movie file consists entirely os a single movie atom, which in turn contains a movie data reference alias atom (of type MovieDataRefAliasAID). This atom contains a single data reference atom (of type DataRefAID]. Finally, the data reference atom is a leaf atom that contains the type of the data reference followed immediately by the data reference itself. Figure 8.1 shows the general structure of a shortcut movie file. It's easy enough to create a file that has this structure. The only thing we don't yet know is what a data reference is, and one must be put into the data reference atom. In the next chapter, we'll take a long look at data references; for the moment, we'll just suppose that a suitable data reference is passed to us. (To look ahead a bit, a data reference is a handle to some data that picks out a movie file or other file. For instance, a URL data reference is a handle to the NULL-terminated string of characters in the URL.) Listing 8.8 shows the f u n c t i o n QTShortCut CreateShortcutMovieFile that takes that data reference and data reference type and then builds a shortcut movie file.
x + 16
x +8
x
moov mdra dref Figure
8.1
Data reference atom data
The structure of a shortcut movie file.
Shortcut Movie Files 2 2 7
Listing 8.8 Creating a shortcut movie file. OSErr QTShortCut CreateShortcutMovieFile (Handle theDataRef, OSType theDataRefType, FSSpecPtr theFSSpecPtr) m
{
long OSErr
myVersion = OL; myErr = noErr;
myErr = Gestalt(gestaltQuickTime, &myVersion) ; i f (myErr ! = noErr) goto bai I ; i f (((myVersion >> 16) & Oxffff) >= Ox0400) { / / we're running under QuickTime 4.0 or greater myErr = CreateShortcutMovieFile(theFSSpecPtr, sigMoviePl ayer, smCurrentScri pt, createMovieFi leDeleteCurFi le I createMovieFi leDontCreateResFi le, theDataRef, theDataRefType) ; } else { / / we're running under a version of QuickTime prior to 4.0 OSType myDataRefType; unsigned l o n g myAtomHeaderSi ze; Ptr myData = NULL; Handle myAtom = NULL; / / create the atom data that goes into a data reference atom (we will create this / / atom's header when we create the movie atom that contains i t ) ; the atom data is / / the data reference type followed by the data reference i t s e l f myDataRefType = EndianU32 NtoB(theDataRefType) ; myAtomHeaderSize = 2 * sizeof(long); m
/ / allocate a data block and copy the data reference type and data reference into i t myData = NewPtrClear(sizeof(OSType) + GetHandleSize(theDataRef)) ; i f (myData == NULL) goto bai I ; BlockMove(&myDataRefType, myData, sizeof(OSType)) ; BlockMove(*theDataRef, (Ptr) (myData + sizeof(OSType)), GetHandleSize(theDataRef)) ; / / create a handle to contain the size and type fields of the movie atom, as well as / / the size and type fields of the movie data reference alias atom contained in i t / / and of the data reference atom contained in the movie data reference alias atom myAtom = NewHandleClear(3 * myAtomHeaderSize);
228
Chapter 8 The Atomic Caf~
i f (myAtom == NULL) goto bai I ; / / f i l l in the size and type fields of the three atoms *((long *) (*myAtom + OxO0)) = EndianU32_NtoB((3 * myAtomHeaderSize) + GetPtrSi ze (myData)) ; *((long *) (*myAtom + Ox04)) = EndianU32 NtoB(MovieAID) ; *((long *) (*myAtom + Ox08)) = EndianU32 NtoB((2 * myAtomHeaderSize) + GetPtrSi ze (myData)) ; *((long *) (*myAtom + OxOC)) = EndianU32 NtoB(MovieDataRefAliasAID) ; *((long *) (*myAtom + OxlO)) = EndianU32_NtoB((1 * myAtomHeaderSize) + GetPtrSi ze (myData)) ; *((long *)(*myAtom + 0x14)) = EndianU32 NtoB(DataRefAID); m
/ / concatenate the data in myData onto the end of the movie atom myErr = PtrAndHand(myData, myAtom, GetPtrSize(myData)) ; i f (myErr ! = noErr) goto bai I ; / / create the shortcut movie f i l e myErr = QTShortCut WriteHandleToFile(myAtom, theFSSpecPtr) ; bail: i f (myData ! = NULL) Di sposePtr (myData) ; i f (myAtom!= NULL) Di sposeHandl e (myAtom) ;
return (myErr) ;
Our function QTShortCut CreateShortcutMovieFile calls the Movie Toolbox function CreateShortcutMovieFile if it's available; otherwise, it creates the movie atom itself, by building up three consecutive atom headers and then appending the data reference type and data onto the end of those atom headers. It writes the entire movie atom into the specified file by calling the QTShortCut_WriteHandleToFile function. (We've already encountered a version of this function, called QTDX_WriteHandleToFile; see Chapter 5, "In and Out.")
Atom Containers QuickTime version 2.1 introduced a new way to store information that greatly facilitates creating hierarchical collections of data and retrieving data
Atom Containers 229
from those collections. The basic ideas are very simple: at the root of the data hierarchy is an object called an atom container. Inside of an atom container are other objects, called atoms. (If we need to distinguish these atoms from the ones we've been considering up to now, we'll call them atom container atoms.} An atom can contain other atoms, in which case it is a parent atom. The atoms that are contained within a parent atom are called its child atoms. If an atom contains only data (and no other atoms}, it is a leaf atom. The data in a leaf atom is always in big-endian format. (Well, almost always; we'll encounter an exception to this rule in a little while.} An atom has an atom type and an atom ID. A parent atom can contain any n u m b e r of children of any type and ID. The only restriction is that, for a given parent, no two children can have the same type and ID. So we can uniquely identify an atom by specifying its parent, its type, and its ID. (For an atom that is contained directly in the atom container, the atom container is considered to be the atom's parent; the special constant kParentAtomIsContainer is used to signal this fact.} We can also identify a particular atom by specifying its parent, its type, and an index of atoms of that type in that parent. (QuickTime supports yet a third method of identifying atoms, using the atom's position in the atom container, called its offset; we w o n ' t consider this way of identifying atoms here.} Let's consider a few examples. Figure 8.2 shows a very simple collection of data, where the atom container has just two children, each of which holds a long integer that represents the length (in millimeters} of a lizard. These leaf atoms both have the atom type '1 z] n'. Figure 8.3 shows a more complicated arrangement of data. In this case, the root atom container contains two parent atoms, both of type ']dat' (for "lizard data"}. Within each ']dat' atom are two children, which have different types. The atom of type 'lzln' contains a long integer (as in Figure 8.21, and the atom of type ']nam' (for "lizard name"} contains a string of characters. {This is neither a C string nor a Pascal string; it's just the characters themselves.} Notice that both ']zln' atoms have an atom ID of 1; this is OK, since those atoms have different parents. Atom containers can be vastly more complicated than the ones shown in Figures 8.2 and 8.3, and they don't have to exhibit the kind of nice symmetry we see there. On the other hand, some real-life atom containers are just that simple. But no matter how complicated they are, we'll use the same functions to build atoms and atom containers and to retrieve data from atom containers. Let's see how to accomplish these tasks.
238
Chapter8 The Atomic Caf~
I Atom container I
I
~,o~,ype ~,o~,0 ~,o~,~
I
~l~z~nl ;1~ I ~1~0~1
I
I~z~nl I~ I I~ I
Figure 8.2 A simple atom container.
I Atom container I
I
Idat 1
]Z
In
lz In
In am
]
Avl'il
i0 !9
I
Inam
]
1
12 i3
May
Figure 8.3 A more complex atom container.
Creating
Atom
Containers
The file Movies.h defines these types for w o r k i n g w i t h a t o m s and a t o m containers: typedef typedef typedef typedef
Handle 1ong long long
QTAtomContai ner; QTAtom; QTAtomType; QTAtomID;
Notice that an a t o m container is just a handle to some data (structured in a specific way, to be sure). This m e a n s that we can d e t e r m i n e the size of an a t o m container by using the function GetHandl eSi ze. T h a t ' s about as m u c h as we need to k n o w about the w a y an a t o m container is stored in m e m o r y . The actual structure of an a t o m container is publicly d o c u m e n t e d , but t h a n k f u l l y w e will not need to learn a n y t h i n g about that structure. The Movie Toolbox provides all the functions we'll n e e d in order to create and use a t o m s and a t o m containers.
Atom Containers
231
We create an atom container by calling the QTNewAtomContainer function, like this: QTAtomContainer myAtomContainer= NULL; myErr = QTNewAtomContainer(&myAtomContai ner) ;
If QTNewAtomContainer completes successfully, then the value of the variable myAtomContainer is a new, e m p t y atom container. We can then add atoms to that container by calling QTInsertChild. For instance, to add the two children shown in Figure 8.2 to this atom container, we could execute this code: myLong = EndianU32 NtoB(1029); myErr = QTInsertChild(myAtomContainer, kParentAtomlsContai ner, kLizardLength, 1, O, sizeof(myLong), &myLong, NULL); myLong = EndianU32 NtoB(1253) ; myErr = QTInsertChild(myAtomContainer, kParentAtoml sContai ner, kLizardLength, 2, O, sizeof(myLong), &myLong, NULL);
The second p a r a m e t e r to the QTlnsertChild function specifies the parent atom of the child w e ' r e inserting. Here, you'll notice, w e ' r e using the constant kParentAtomIsContainer to indicate that the parent atom is the atom container itself. The third and fourth parameters specify the type and ID of the new atom. The fifth p a r a m e t e r is the desired index of the new atom within the parent atom; we don't care about the index here, so we pass the value 0 to indicate that the new atom is to be inserted as the last child of the specified type in the parent atom. The sixth and seventh parameters to QTInsertChi l d specify the n u m b e r of bytes of data to be added to the atom, along with a pointer to the atom data itself. The last p a r a m e t e r is a pointer to a variable of type QTAtom, in which QTInsertChi l d will return to us an identifier for the new atom; we don't need that information here, so we pass NULLin that parameter. We can create a hierarchy within an atom container by inserting parent atoms and then adding some children to those parents. We also call QTInsertChild to insert a parent atom, but we do not need to specify any data or data size; instead, we specify a variable of type QTAtom in w h i c h the identifier of the new parent atom is returned to us. Here's an example:
232
Chapter8 The Atomic Caf~
QTAtom myLi zardAtom; myErr = QTInsertChild(myAtomContainer, kParentAtoml sContai ner, kLizardData, 1, 1, O, NULL, &myLizardAtom) ;
Then we can insert a child atom into this parent atom, like this: myErr = QTInsertChild(myAtomContainer, myLi zardAtom, kLizardName, 1, 1, strl en (theLi zardName), theLizardName, NULL);
Note that the second p a r a m e t e r here is the parent atom that we just created. If we insert another atom (this time of type '1 z ln') into the parent and then repeat the whole process for the second lizard, we would have the atom structure shown in Figure 8.3.
Finding Atoms in A t o m Containers If we are given an atom container, it's almost as easy to get data out of it as it is to put data into it. First we need to find the atom whose data we want. The standard way to do this is to start at the top of the hierarchy and gradually descend until we find the parent of the desired atom. Then we can get an atom identifier of the target atom by calling the QTFindChildByID function. For example, if myLizardAtom is the parent atom for the atoms that hold the data about our lizard Avril, then we can get the n a m e atom by executing this code: myNameAtom = QTFindChi I dByID(myAtomContai ner, myLizardAtom, kLi zardName, 1, NULL); QTFindChildBylD actually inspects both the type and ID passed to it (not just the ID, as the name might suggest). The Movie Toolbox provides a n u m b e r of other functions that are useful for finding specific atoms, including QTCountChi 1dren0fType, QTFindChi 1dByIndex, QTGetNextChi I dType, and QTNextChi I dAnyType.
Getting A t o m Data Once we've found a leaf atom, we can get the data from that atom in several ways. If we want a copy of the atom data that will persist even after w e ' v e disposed of the atom container, we can call QTCopyAtomOataToHandle or
Atom Containers 2.33
QTCopyAtomDataToPtr, passing in a handle or pointer to a block of m e m o r y that's big enough to hold the leaf atom data. If, on the other hand, we just want to look at the atom data and don't need a copy of it, we can call the QTGetAtomDataPtr function, w h i c h returns a pointer to the actual leaf atom data. If you plan to make calls that might move memory, then you should call QTLockContainer before calling QTGetAtomDataPtr; then call QTUnlockContai ner w h e n you are done with the data pointer. If we want to retrieve our lizard's name, we could make this call: QTGetAtomDataPtr(myAtomContainer, myNameAtom, &myNameSize, &myNameData); If QTGetAtomDataPtr completes successfully, then myNameData points to the string of characters that make up the name, and myNameSize contains the size of that name.
Internet Connection Speed For our first real-life encounter with atom containers, let's consider h o w to get and set the user's Internet connection speed preference. The user can set a preference in the Connection Speed panel of the QuickTime Settings control panel, shown in Figure 8.4. QuickTime uses this setting for various purposes. For instance, if a user wants to play an alternate data rate movie file located on a remote server, QuickTime uses this connection speed to select the correct target movie. (An alternate data rate movie lile is a movie file that references other movies, each tailored for downloading across a connection of a certain speed.) We can retrieve the user's current QuickTime preferences by calling the GetQui ckTimePreference function, like this:
myErr = GetQuickTimePreference(ConnectionSpeedPrefsType, &myPrefsContainer) ; The first p a r a m e t e r specifies the kind of preference we wish to retrieve, and the second p a r a m e t e r is the address of an atom container in w h i c h the requested preference data is returned. It's up to us to dispose of that atom container w h e n we are done reading data from it. In the present case, w h e n we retrieve the Internet connection speed, the atom container contains an atom of type ConnectionSpeedPrefsType, whose data is structured as a record of type ConnectionSpeedPrefsRecord, defined like this:
struct ConnectionSpeedPrefsRecord { 1ong connecti onSpeed;
}~
234
Chapter8 The Atomic Caf~
Figure 8.4 The Connection Speedpanel. This record contains a single field that indicates the n u m b e r of bytes per second that the user's Internet connection can support. The file MoviesFormat.h defines a set of c o m m o n values: enum { kDataRate144ModemRate kDataRate288ModemRate kDataRateISDNRate kDataRateDuaIISDNRate kDataRateTIRate kDataRatelnfiniteRate
};
= = = =
1400, 2800, 5600, 11200,
= 150000L,
= Ox7FFFFFFF
Once we've received an atom container from GetQuickTimePreference, we can use the QTFindChildByID function to find the child atom of type ConnectionSpeedPrefsType. Then we get the atom data by calling the QTGetAtomDataPtr function. Finally, we can read the value stored in the connectionSpeed field, to find the current connection speed preference. We'll return this value
Internet Connection Speed 235
as the function result, w h e t h e r or not it's one of the predefined c o m m o n values. If any error occurs, however, we'll return the value kDataRate288ModemRate, which is a reasonable default. Listing 8.9 shows the complete function OTUti I s GetUsersConnectionSpeed. u
Listing 8.9 Getting the user's Internet connection speed preference. long QTUtils GetUsersConnectionSpeed (void) I
QTAtomContai ner QTAtom Connect i onSpeedPrefsRecord long long Ptr OSErr
myPrefsContai ner= NULL; myPrefsAtom = O; myPrefsRec; myDataSize = OL; mySpeed = kDataRate288ModemRate; myAtomData = NULL; myErr = noErr;
myErr = GetQuickTimePreference(ConnectionSpeedPrefsType, &myPrefsContainer) ; i f (myErr == noErr) { / / find the atom of the desired type myPrefsAtom = QTFindChildByID(myPrefsContainer, kParentAtomlsContainer, ConnectionSpeedPrefsType, 1, NULL); i f (myPrefsAtom ! = O) { / / read the data contained in that atom and verify that the data is of the / / size we are expecting QTGetAtomDataPtr(myPrefsContainer, myPrefsAtom, &myDataSize, &myAtomData); i f (myDataSize == sizeof(ConnectionSpeedPrefsRecord)) { / / read the connection speed myPrefsRec = *(ConnectionSpeedPrefsRecord *)myAtomData; mySpeed = myPrefsRec,connecti onSpeed;
}
QTDisposeAtomContai ner (myPrefsContai ner); return (mySpeed) ;
Note that we haven't performed any endian swapping on the value we read from the connection speed preferences record. That's because the data in this particular atom container is stored in native-endian format. This is an exception to the general rule that data in atom containers is big-endian. A user's QuickTime preferences are not designed to be moved from machine to machine, so there is no need to enforce big-endian byte ordering.
236
Chapter 8 The Atomic Caf~
Listing 8.10 shows how we can set a user's Internet connection speed. In general, we should let the user decide the connection speed preference, but it can sometimes be useful to do this programmatically. Listing 8.10 Setting the user's Internet connection speed preference. OSErr QTUtils SetUsersConnectionSpeed (long theSpeed)
{
m
QTAtomContainer Connecti onSpeedPrefsRecord OSErr
myPrefsContainer = NULL; myPrefsRec; myErr = noErr;
myErr = QTNewAtomContainer (&myPrefsContai ner); i f (myErr == noErr) { . myPrefsRec.connectionSpeed = theSpeed; myErr = QTInsertChild(myPrefsContainer, kParentAtomlsContainer, ConnectionSpeedPrefsType, 1, O, si zeof (Connecti onSpeedPrefs Record), &myPrefsRec, NULL); i f (myErr == noErr) myErr = SetQuickTimePreference(ConnectionSpeedPrefsType, myPrefsContainer) ; QTDisposeAtomContai ner (myPrefsContai ner) ; return (myErr) ;
QTUtils SetUsersConnectionSpeed creates a new atom container, inserts a single child atom into the container that holds the desired speed, and then passes that container to the SetQuickTimePreference function. Once SetQuickTimePreference returns, we can safely call QTDisposeAtomContainer to dispose of the atom container we created. u
Movie Tracks You might recall that in Chapter 3, "Out of Control," we saw how to write an application that plays one QuickTime movie inside of another QuickTime movie. The embedded movie (what we then called the "picture-in-picture movie") could have looping characteristics different from those of the main movie. For example, the embedded movie could keep looping over and over while the main movie played through once and then stopped. And, in theory, the embedded movie could play at twice its normal speed, while the
Movie Tracks 237
Figure 8.5 A child movie inside of a parent movie.
main movie played at, say, half its normal speed. (We didn't actually provide this alternate speed capability, but it would be easy enough to add.) The only drawback to this was that we needed the special playback application QTMooVToolbox to make it all happen. Wouldn't it be nice if we could create movie files with these capabilities, so that they would play back using any QuickTime-savvy application? This is precisely what's offered in QuickTime 4.1 with the introduction of movie tracks managed by the movie media handler. By adding movie tracks to an existing QuickTime movie, we can effectively embed an entire QuickTime movie into that movie. (This capability is sometimes called the movie-in-movie capability; the embedded movie is also called the child movie, while the main movie is also called the parent movie.) Figure 8.5 shows one movie embedded within another movie using a movie track. Remember that the looping characteristics and playback rate of a movie are associated with the movie's time base. Prior to QuickTime 4.1, it was possible to create movies with overlaid video tracks, but all the tracks in the movie shared the same time base. The time base of the overlaid track is slaved to that of the other tracks. What movie tracks bring to the table is the ability to have nonslaved time bases in a single movie. That is to say, each child movie can have its own time base, resulting in looping and playback rate characteristics independent of those of the parent movie.
Adding a Movie Track to a Movie So let's see how to create movie tracks. Suppose that theWindowObject is a window object for an open QuickTime movie file and that theDataRef and
238
Chapter 8 The Atomic Caf~
theDataRefType are a data reference and a data reference type for some other QuickTime movie file. Then we can call the QTMIM_AddMovieTrack function defined in Listing 8.11 to add to that open movie a movie track that references that file. (Once again, we'll postpone discussing data references to the next chapter; for now, all we need to k n o w is that they pick out QuickTime movie files, either on the local machine or elsewhere on the Internet.) Listing 8.11 Adding a movie track to a QuickTime movie. OSErr QTMIM AddMovieTrack (WindowObject theWindowObject, OSType theDataRefType, Handle theDataRef) m
Movie Track Media OSErr if
myMovie myTrack myMedia myErr =
= NULL; = NULL; = NULL; paramErr;
((theWindowObject : : NULL) goto bai I ;
II
/ / the parent movie / / the movie track / / the movie track's media
(theDataRef := NULL))
myMovie = (**theWindowObject).fMovie; / / create the movie track and media myTrack = NewMovieTrack(myMovie, FixRatio(kChildMovieWidth, 1), FixRatio(kChildMovieHeight, 1), kFulIVolume) ; myErr = GetMoviesError() ; i f (myErr ! = noErr) goto bai I ; myMedia = NewTrackMedia(myTrack, MovieMediaType, kMovieTimeScale, NULL, 0); myErr = GetMoviesError() ; i f (myErr ! = noErr) goto bai I ; / / create the media sample(s) myErr = Begi nMedi aEdi ts (myMedia) ; i f (myErr ! = noErr) goto bai I ; myErr = QTMIM AddMovieTrackSampl eToMedi a (theWi ndowObject, myMedia, theDataRefType, theDataRef); i f (myErr != noErr) goto bai I ; m
myErr : EndMediaEdi ts (myMedia) ; i f (myErr ! = noErr) goto bai I ;
Movie Tracks 2 3 9
/ / add the media to the track myErr = InsertMedialntoTrack(myTrack, O, O, GetMediaDuration(myMedia), fixed1); bail 9 return (myErr) ;
}
There is absolutely nothing new about this function. It's virtually identical to the function QTMM_CreateVideoMovie that we e n c o u n t e r e d in an earlier chapter (Chapter 6, "Doug's 1st Movie"). The only real difference is that we've created a media of type MovieMediaType; also, here we call QTMIM_AddMovieTrackSampleToMedia to add media samples to the new track, while earlier we called QTMM_AddVideoSampl esToMedi a.
Creating a M o v i e Track M e d i a Sample By now you might be wondering w h a t this has to do with atom containers. The answer is simple: the media sample for a movie track consists of an atom container whose atoms specify the movie to be e m b e d d e d in the main movie, as well as some of the playback characteristics of the e m b e d d e d movie. In other words, the function QTMIM_AddMovieTrackSampl eToMedi a needs only to create an appropriate atom container and pass that container to the AddMedi aSampl e function. We'll begin, therefore, by calling QTNewAtomContainer to create a new atom container; since this container will serve as our media sample, we'll call it mySampl e: myErr = QTNewAtomContainer(&mySample) ;
Into this new atom container, we want to put an atom of type kMovieMediaDataReference, whose data consists of the data reference type and the data
reference of the movie file that is to be the e m b e d d e d movie. We can create the atom data like this" myData = NewPtrClear(sizeof(OSType) + GetHandleSize(theDataRef)) ; myType = EndianU32 NtoB(theDataRefType); BlockMove(&myType, myData, sizeof(OSType)) ; BlockMove(*theDataRef, myData + sizeof(OSType), GetHandleSize(theDataRef)) ;
Then we can insert the atom into the atom container by calling QTInsertChild: myErr = QTlnsertChild(mySample, kParentAtomlsContainer, kMovieMediaDataReference, 1, 1, GetPtrSize(myData), myData, NULL);
240
Chapter8 The Atomic Caf~
At this point, we could call AddMediaSample to add the atom container mySample as the single media sample of the movie track. But we would like the embedded movie to start playing automatically w h e n the parent movie reaches the start time of the movie track, which is not the default behavior. To have the embedded movie automatically start playing, we need to add another atom to the atom container, of type kMovieMediaAutoPlay. myBool ean = true; myErr = QTInsertChild(mySample, kParentAtomlsContainer, kMovieMediaAutoPlay, 1, I, sizeof(myBoolean), &myBoolean, NULL);
Now we can create a sample description and add the atom container to the movie track media. Listing 8.12 shows our function OTMIM_AddMovieTrackSampleToMedia for adding a media sample to a movie track. Listing 8.12. Adding a sample to the movie track media. OSErr QTMIM_AddMovieTrackSampl eToMedia (WindowObject theWindowObject, Media theMedia, OSType theDataRefType, Handle theDataRef)
(
#pragma unused(theWi ndowObject) QTAtomContai ner QTAtom SampleDescriptionHandle Ptr OSType Boolean OSErr
mySample = NULL; myRegionAtom; mylmageDesc = NULL; myData = NULL; myType; myBool ean; myErr = paramErr;
/ / create a new atom container to hold the sample data myErr = QTNewAtomContainer (&mySample) ; i f (myErr l= noErr) goto bai I ; / / concatenate the data reference type and data reference into a single block of data myData = NewPtrClear(sizeof(OSType) + GetHandleSize(theDataRef)) ; i f (myData == NULL) goto bai I ; / / convert the data to big-endian format myType = EndianU32 NtoB(theDataRefType); D
BlockMove(&myType, myData, sizeof(OSType)) ; BlockMove(*theDataRef, myData + sizeof(OSType), GetHandleSize(theDataRef)) ;
Movie Tracks 24'11
/ / add an atom of type kMovieMediaDataReference to the atom container myErr = QTInsertChild(mySample, kParentAtomlsContainer, kMovieMediaDataReference, 1, 1, GetPtrSize(myData), myData, NULL); i f (myErr ! = noErr) goto bai I ; / / add an auto-start atom myBool ean = true; myErr = QTInsertChild(mySample, kParentAtomlsContainer, kMovieMediaAutoPlay, 1, 1, sizeof(myBoolean), &myBoolean, NULL); i f (myErr ! = noErr) goto bai I ; / / create a sample description myImageDesc = (SampleDescriptionHandle)NewHandleClear(sizeof(SampleDescription)) ; i f (mylmageDesc == NULL) goto bail ; (**myImageDesc).descSize = sizeof(SampleDescription) ; (**my ImageDesc). dataFormat = MovieMedi aType; myErr = AddMedi aSample( t heMedi a, mySample, O, GetHandleSize( (Handl e)mySample), GetMovi eDurat i on (GetTrac kMovi e (GetMedi aTrac k (theMedi a) ) ), myImageDesc, 1, O, NULL); bail: i f (myData l= NULL) Di sposePtr (myData) ; i f (mylmageDesc ! = NULL) Di sposeHandl e ( (Handl e)mylmageDesc) ;
242
Chapter8 The Atomic Caf~
i f (mySample ! = NULL) QTDi sposeAtomContai ner (mySample) ; return (myErr) ;
Notice that, as hinted at earlier, we call GetHand]eSize to get the size os the atom container when we call AddMediaSamp] e.
T h e Code In this chapter, our sample code is scattered across four different sets of files. The updated version of QTInfo is called QTInfoPlus and includes a new version of the QTInfo MakeFi]ePreview function and all the utilities that we used to access the atoms in a QuickTime movie file. The code for creating shortcut movie files is contained in the snippet QTShortcut.c. The code for getting and setting the user's Internet connection speed preference is contained in the file QTUtilities.c (which has been part of every project we've developed so far). Finally, the code for adding movie tracks to QuickTime movies is contained in the project for the sample application QTMovieTrack. QTMovieTrack allows the user to configure a large number of settings for the new movie track, in addition to the auto-playback setting (which we considered earlier). Figure 8.6 shows the dialog box that QTMovieTrack displays to allow the user to configure a new movie track. D
Figure 8.6 QTMovieTrack'sMovie Track Propertiesdialog box. The Code 2431
Conclusion QuickTime tries very hard to insulate us from having to work directly with the kinds of atoms that comprise movie files {the so-called classic, or chunk, atoms]. The Movie Toolbox provides an extensive set of high-level routines that we can use to create new movie files, open existing movie files, edit movie files, and so forth. Nevertheless, we've seen that there are occasions w h e n we do need to interact with atoms directly. A good example of this concerns adding a file preview to a single-fork movie file. QuickTime currently provides no API to do this, so we are forced to w o r k with the file data--the atoms--directly. Similarly, if we want to create a shortcut movie file on a machine that's running a version of QuickTime prior to version 4.0, we need to work with atoms. By contrast, we'll encounter atom containers and their associated atom container atoms at virtually every forward step in our journey through QuickTime. Atom containers and their children provide an easy way to maintain hierarchical data, and they're backed by an extensive programming interface. So atom containers are now the repository of choice for storing and exchanging data. We'll see them used in media samples, tween tracks, input maps, musical instruments, wired actions, video effect tracks, and for a large n u m b e r of other uses. Internet connection speed preferences and movie tracks are just the tip of the ice floe.
2414 Chapter8 The Atomic Caf~
Somewhere I'll Find You
Introduction In the previous few chapters, we've learned that a movie is composed of tracks and that each track is associated with one specific kind of media data. During movie playback, QuickTime uses a media handler (a component of type MediaflandlerType) to interpret the media data and present it to the user in the appropriate manner. For example, if the media data for some particular track consists of compressed video data, the video media handler calls the decompressor specified in the image description and then draws the decompressed frames in the appropriate location. A media handler, however, does not typically concern itself with reading the media data from the movie file (or from wherever else the media data is stored). Instead, the media handler gets that data from a data handler (a component of type DataHandlerType), which is responsible for reading and writing a media's data. In other words, a data handler provides data input and output services for a media handler and for other parts of QuickTime. We identify a source of media data to a data handler by providing a data reference. As we'll see shortly, QuickTime currently includes five standard data handlers. Each data handler works with one specific sort of data reference. For instance, the URL data handler expects data references that are handles to URLs. That is to say, a URL data reference is a handle to a block of data that contains a NULL-terminated string of characters. The other data handlers expect their references in other forms. A typical movie file contains data references to its media data in a data information atom, which is nested deep inside each track atom in the movie atom. In the previous chapter, we saw that a shortcut movie file contains a data reference atom that identifies an entire movie file. Data references can in fact pick out any kind of data, not just media data. In general, if we want to tell QuickTime where to find some data or where to put some data, we'll use a data reference to do so.
245
Figure 9.1
The Test menu of QTDataRef.
In this chapter, we're going to see h o w to work with each of the five standard data handlers. At the very simplest level, this involves nothing more than creating an appropriate data reference and putting that reference into a file (as we did in the last chapter) or passing it to a Movie Toolbox function like NewMovieFromDataRef. So we'll begin by learning how to create data references and call a few of the FromDataRef functions. Then we'll take a look at some of the functions that we can use to work with data handlers directly. These are fairly low-level functions that we w o n ' t need to use very often. Here, for fun, we'll see how to use them to write a general-purpose, asynchronous file transfer utility that relies solely on QuickTime APIs. Finally, toward the end of this chapter, we'll take a brief look at data reference extensions, which are blocks of additional data that we can associate with a data reference to assist QuickTime in working with the data picked out by the data reference. As we'll see, data reference extensions can be especially useful to graphics importers, to help them avoid having to take the time to validate some block of image data. Our sample application for this chapter is called QTDataRef; it illustrates how to work with data references and data handlers. Figure 9.1 shows the Test m e n u from QTDataRef.
Data Handler Overview W h e n QuickTime was first released, it included only one data handler, the lile data handler, which can read and write data stored in files on a local file system. QuickTime version 2.0 introduced the handle data handler, which allows the Movie Toolbox to work with data stored in m e m o r y and referenced with a handle. QuickTime version 2.5 added the resource data handler, which can read data stored in a file's resource fork. QuickTime 3.0 added the URL data handler to support reading data from locations specified using uniform resource locators fURLs). More recently, QuickTime 5 added the
246
Chapter 9 Somewhere I'll Find You
pointer data handler, which can w o r k with data stored in m e m o r y and referenced with a pointer. All data handlers are components of type DataHandlerType. Data h a n d l e r s are distinguished from one another by their c o m p o n e n t s u b ~ e s . The file Movi es.h defines constants for four of the five data handler subtypes: enum { HandleDataHandlerSubType ResourceDataHandlerSubType URLDataHandlerSubType PointerDataHandlerSubType
};
= = = =
FOUR CHAR CODE('hndl ' ) , FOUR CHAR CODE('rsrc'), FOUR CHAR CODE('url ' ) , FOUR CHAR CODE('ptr') D
And the file A] i ases. h defines the constant used for the file data handler: enum { rAl i asType
};
= FOUR CHAR CODE('alis')
QuickTime also includes several other data handlers that it uses for its own private purposes and for which there is currently no public API. We w o n ' t consider these private data handlers in this chapter, but you should at least know that they exist [so that, for example, you aren't surprised if you iterate through all components of type DataHandlerType and find more than five of them). A data reference is a handle to a block of m e m o r y that uniquely identifies the location of some media data for a QuickTime movie or some other data that QuickTime can manage. QuickTime currently provides support for five types of data references, one for each of the available data handlers. To let the cat out of the bag: 9 A file data reference is a handle to an alias record that specifies a file on a local storage volume (or on a remote storage volume m o u n t e d on the local machine). That is to say, a file data reference is an alias handle. Because the file data handler is of subtype rA] i asType and uses alias handles as its d a t a r e f e r e n c e s , it is often called the alias data handler. In a d d i t i o n , because it originally handled files on the Macintosh hierarchical file system (HFS), it is also sometimes called the HFS data handler. 9 A handle data reference is a handle to a 4-byte block of m e m o r y that holds a handle to some other block of data. That is to say, a handle data reference is a handle to a handle. 9 A resource data reference is a handle to an alias record to which two pieces of information have been appended, a resource type and a resource ID.
Data Handler Overview 247
The target data is found in the resource fork of the file specified by that alias record, in the resource of the specified type and ID.
9 A URL data reference is a handle to a NULL-terminated string of characters that comprise the URL. The string of characters should conform to the relevant Internet Engineering Task Force (IETF) specifications; in particular, some nonalphanumeric characters may need to be encoded using the hexadecimal equivalents of their ASCII codes (for instance, the space character should be encoded as "~20"). 9 A pointer data reference is a handle to a data structure of type PointerDataRefRecord, which specifies a location in m e m o r y and a length. In the following five sections, we'll investigate these five types of data references in more detail. Before we begin, however, let's introduce a bit of terminology that will be useful throughout this chapter. Let's call the block of memory to which a data reference is a handle the referring data. And let's call the data picked out by the data reference the target data (or just the target) of the data reference. So, for example, the referring data of a URL data reference is the string of characters, and the target data of a URL data reference is the data in the file picked out by that URL.
T h e File D a t a H a n d l e r We can use the file data handler to open movies that are specified using a file data reference, which is an alias handle. Listing 9.1 shows how to create a file data reference for a given file.
Listing 9.1 Creating a file data reference. Handle QTDR_MakeFileDataRef (FSSpecPtr theFile)
{
Handle
myDataRef = NULL;
QTNewAlias(theFile, (AliasHandle *)&myDataRef, true) ; return (myDataRef) ;
QTDR_MakeFi l eDataRef consists mainly of a call to the Movie Toolbox function QTNewA1i as, which returns, in the location specified by the second parameter, an alias handle for the file specified by the first parameter. The third parameter is a Boolean value that indicates whether to create a minimal or a full alias record. (A minimal alias record contains only the minimum infor-
248
Chapter 9 Somewhere I'll Find You
mation needed to find a file; it's generally m u c h faster to find the target of a minimal alias record, so that's w h a t we'll use here.) We could also have used the Alias M a n a g e r functions NewAlias (for a full alias record) or NewA]iasMinima] (for a m i n i m a l alias record) to create the file data reference. The principal advantage of using QTNewA]i as is that it r e t u r n s an alias handle even if the file specified by t h e F i l e doesn't yet exist. Both NewAlias and NewAliasMinimal r e t u r n an error (fnfErr)--and do not create an alias record--if the specified file does not exist. As we'll see soon, we sometimes w a n t to create data references for files that we h a v e n ' t yet created. {The Alias Manager does provide the NewAliasMinimalFromFullPath function that can create alias records for files that don't exist, but w e ' d r a t h e r not have to deal with full p a t h n a m e s just to do that.)
O p e n i n g a M o v i e File Let's consider a few examples of using file data references. First, w e can pass a file data reference to the NewMovieFromDataRef function, to achieve exactly the same effect as calling the OpenMovieFile and NewMovieFromFile functions. Listing 9.2 defines the function QTDR_GetMovieFromFi l e, w h i c h creates a file data reference and then retrieves the movie in the specified movie file.
Listing 9.2 Opening a file specified by a file data reference. Movie QTDR_GetMovieFromFile (FSSpecPtr theFile)
{
Movie Handle
myMovie = NULL; myDataRef= NULL;
myDataRef = QTDRMakeFileDataRef(theFile) ; i f (myDataRef l= NULL) { NewMovieFromDataRef(&myMovie, newMovieActive, NULL, myDataRef, rAliasType) ; Di sposeHandle (myDataRef) ;
}
return (myMovie) ;
The last two parameters to the NewMovieFromDataRef function specify, respectively, the data reference and the data reference type. Since we are passing it a file data reference, we set the data reference type to rA] i asYype. Note that we call Di sposeHandl e to dispose of the data reference once we are finished using it. By the time NewMovieFromDataRef returns, the Movie Toolbox will have m a d e a copy of the data reference, if necessary.
The File Data Handler
249
Creating a R e f e r e n c e M o v i e File Now let's turn to a more interesting example of using file data references. We learned in Chapter 6, "Doug's 1st Movie," that a movie file might not contain the media data referenced by the movie's tracks; instead, that media data might be contained in some other file. A file that contains some media data is a media/ile, and the file that contains the movie atom is the movie file. (For simplicity, let's assume that our movies have only one track, a video track.) W h e n the movie file and the media file are different files, the movie file is a reference movie file (because it refers to its media data and does not contain it). W h e n the movie file and the media file are the same file, the movie file is a self-contained movie lile. While, generally, we prefer to create self-contained movie files, it's useful to know how to create a reference movie file. (Reference movie files can be useful, for instance, if we want to share some media data among several QuickTime movies.) One way to do this is to pass a file data reference to the NewTrackMedia function. Recall that NewTrackMedi a is declared essentially like this: Media NewTrackMedia (Track theTrack, OSType mediaType, TimeScale timeScale, Handle dataRef, OSType dataRefType) ; The fourth and fifth parameters specify the data reference and the data reference type of the file (or other container) that is to hold the media data for the specified track. Previously, w h e n e v e r we called NewTrackMedia, we passed NULLand 0 in those parameters, to indicate that the media file should be the same as the movie file. Now we'll create a reference movie file simply by specifying a media file that is different from the movie file. We're going to define a function QTOR_CreateReferenceCopy that we can use to create a copy of an existing movie that contains the first video track in the original movie. The copy's movie atom will be contained in one file (theOstMovieFile), and its media data will be contained in some other file (theOstMediaFi l e). QTDR_CreateReferenceCopyhas this prototype" OSErr QTDR_CreateReferenceCopy (Movie theSrcMovie, FSSpecPtr theDstMovieFile, FSSpecPtr theDstMediaFile); The definition of QTDR_CreateReferenceCopy will look vaguely familiar, at least if you recall the sequence of moviemaking functions that we encountered in the QTMM_CreateVideoMovie function. It uses CreateMovieFile, NewMovieTrack, NewTrackMedia, BeginMediaEdits, EndMediaEdits, and AddMovieResource in the standard ways. There are only two important differences
250
Chapter 9 Somewhere I'll Find You
between QTDR_CreateReferenceCopy and QTMM_CreateVideoMovie. First, w e ' r e going to obtain the media data for the copy from the first video track in the existing movie; to do this, we'll call GetMovieIndTrackType and GetTrackMedia to get the media from the source movie:
mySrcTrack = GetMovielndTrackType(theSrcMovie, 1, VideoMediaType, movieTrackMediaType); mySrcMedia = GetTrackMedia(mySrcTrack) ; Then we'll call GetTrackDimensions and GetMediaHandlerDescription to obtain some information about the source track and its media:
GetTrackDimensions(mySrcTrack, &myWidth, &myHeight); GetMediaHandlerDescription(mySrcMedia, &myType, O, 0); W h e n we create the copy movie, we'll need this information to be able to specify the size of the new track and the type of the track's media. The second main difference between OTOR_CreateReferenceCopy and QTMM_CreateVideoMovie concerns the way we add media data to the new movie. Previously, we called an application-defined function to create the media samples and add them to the track's media (using AddMediaSample). Now, however, we've already got the media data for the new file--it's just the media data contained in the first video track of the source movie file. So, instead, we can use the Movie Toolbox function InsertTrackSegment to copy the media data from the source track to the new track, like this:
myErr = InsertTrackSegment(mySrcTrack, myDstTrack, O, GetTrackDuration(mySrcTrack), O) ; Here, InsertTrackSegment copies the entire source track {that is, starting at time zero and extending for the duration GetTrackDuration(mySrcTrack)) and inserts it into the destination track starting at time zero. The result is that the new media file will contain a copy of the source track's media data. There is one final detail we need to attend to. In order for the destination track to have the same visual characteristics as the source track, we also need to copy the source track matrix, clipping region, and graphics mode {among other things} into the destination track. Similarly, if the source track were a sound track, we would need to copy its volume, sound balance, and other sound characteristics into the destination track. We could copy the visual settings by calling GetTrackMatri x and SetTrackMatri x, GetTrackCl i pRgn and SetTrackClipRgn, and so forth. Better yet, the Movie Toolbox provides the CopyTrackSettings function that copies all these settings in one fell swoop:
CopyTrackSettings (mySrcTrack, myDstTrack) ;
The File Data Handler 251
Listing 9.3 shows our comp|ete definition of QTDR_CreateReferenceCopy.
Listing 9.3 Creating a reference movie file. OSErr QTDR_CreateReferenceCopy (Movie theSrcMovie, FSSpecPtr theDstMovieFile, FSSpecPtr theDstMediaFile)
{
Track Medi a Movie Track Medi a Handle Fixed OSType long short short OSErr
mySrcTrack = NULL; mySrcMedia = NULL; myDstMovie = NULL; myDstTrack = NULL; myDstMedia = NULL; myMediaRef = NULL; myWidth, myHeight; myType; myFlags = createMovieFileDeleteCurFile [ createMovieFileDontCreateResFile; myResRefNum = O; myResID = movieInDataForkResID; myErr = paramErr;
/ / get the f i r s t video track and media in the source movie mySrcTrack = GetMovielndTrackType(theSrcMovie, 1, VideoMediaType, movieTrackMediaType) ; i f (mySrcTrack == NULL) goto bai I ; mySrcMedia = GetTrackMedia(mySrcTrack) ; i f (mySrcMedia == NULL) goto bai I ; / / get some information about the source track and media GetTrackDimensions(mySrcTrack, &myWidth, &myHeight); GetMediaHandlerDescription(mySrcMedia, &myType, O, 0); / / create a f i l e data reference for the new media f i l e myMediaRef = QTDR_MakeFileDataRef(theDstMediaFi le) ; i f (myMediaRef == NULL) goto bai I ; / / create a f i l e for the destination movie data and create an empty movie myErr = FSpCreate(theDstMediaFile, sigMoviePlayer, MovieFileType, 0); i f (myErr != noErr) goto bai I ; / / create a f i l e for the destination movie atom myErr = CreateMovieFile(theDstMovieFile, sigMoviePlayer,
252
Chapter 9 Somewhere I'll Find You
smCurrentScript, myFlags, &myResRefNum, &myDstMovie) ; i f (myErr I = noErr) goto bai I ; / / assign the default progress proc to the destination movie SetMovieProgressProc(myDstMovie, (MovieProgressUPP)-l, O) ; / / create the destination movie track and media myDstTrack = NewMovieTrack(myDstMovie, myWidth, myHeight, kNoVolume); myErr = GetMoviesError() ; i f (myErr ! = noErr) goto bai I ; myDstMedia = NewTrackMedia(myDstTrack, myType, GetMediaTimeScale(mySrcMedia), myMediaRef, rAliasType) ; myErr = GetMoviesError() ; i f (myErr I = noErr) goto bai I ; / / copy the entire source track into the destination track; / / this copies the track's media samples into the destination media f i l e myErr = BeginMediaEdi ts (myDstMedia) ; i f (myErr ! = noErr) goto bai I ; myErr = InsertTrackSegment(mySrcTrack, myDstTrack, O, GetTrackDuration(mySrcTrack), i f (myErr ! = noErr) goto bai I ;
0);
CopyTrackSettings (mySrcTrack, myDstTrack) ; myErr = EndMediaEdi ts (myDstMedia) ; i f (myErr I= noErr) goto bai I ; / / add the movie atom to the data fork of the movie f i l e myErr = AddMovieResource(myDstMovie, myResRefNum, &myResID, NULL); bail: return (myErr) ;
}
We call the QTDR_MakeFileDataRef function to create a file data reference for the media file specified by the theDstMediaFi l e parameter, and we pass
The File Data Handler
253
Figure 9.2 The missing data dialog box. that data reference to the NewTrackMedia function, as described earlier. By default, QTDataRef n a m e s the new movie file untitled.mov and the new media file u n t i t l e d . d a t . W h e n a user tries to open untitled.mov, the Movie Toolbox looks for u n t i t l e d . d a t to find the movie's media data. If it cannot find that file, the Movie Toolbox displays the dialog box shown in Figure 9.2. One reason we prefer to create self-contained movie files is to ensure that the media data is always available, so the user w o n ' t ever have to see this dialog box. Before we move on, it's worth mentioning that the Movie Toolbox provides the AddEmptyTrackToMovie function, which we could use in Listing 9.3 instead of NewMovieTrack, NewTrackMedia, and CopyTrackSettings, like this: myErr = AddEmptyTrackToMovie(mySrcTrack, myDstMovie, myMediaRef, rAliasType, &myDstTrack) ; i f (myErr I= noErr) goto bai I ; myDstMedi a = GetTrackMedi a (myDstTrack) ;
AddEmptyTrackToMovie makes a copy of an existing track, either in the same movie or in a different movie; it doesn't actually copy the media data, but it does copy the track settings. The source code files accompanying this chapter provide an alternate version of the QTDRCreateReferenceCopy function that uses AddEmptyTrackToMovie.
The Handle Data Handler The handle data handler is used to read data from and write data to a location in memory. That location is specified using a Macintosh handle. A handle data reference is a handle to that handle. Listing 9.4 shows how to create a handle data reference.
254
Chapter 9 Somewhere I'll Find You
Listing 9.4 Creating a handle data reference. Handle QTDR_MakeHandleDataRef (Handle theHandle)
{
Handle
myDataRef = NULL;
myDataRef = NewHandleClear(sizeof(Handle)) ; i f (myDataRef ! = NULL) BlockMove(&theHandle, *myDataRef, sizeof(Handle)) ; return (myDataRef) ;
Here we allocate a relocatable block of m e m o r y that is the size of a handle (4 bytes} and then copy the handle passed to the function into that block of memory. The result is just what we want, a handle to the original handle. For an even simpler routine, we can replace the middle three lines of code by this one line: PtrToHand(&theHandle, &myDataRef, sizeof(Handle)) ;
The Memory Manager function PtrToHand allocates a new relocatable block of m e m o r y of the specified size and then copies the data specified by its first parameter into that block. In a little while, we'll encounter PtrAndHand, a cousin of PtrToHand that appends the data in a pointer to an existing handle. The handle data handler is useful for m a n y tasks. It's useful for playing movies that can fit entirely into RAM, and it's useful for handling images w h e n the image data resides in m e m o r y and not in a file. In the latter case, we can just create a handle data reference from the handle that holds the image data and pass it to GetGraphicsImporterForDataRef to get a graphics importer that can manage the image data. Similarly, to play a movie stored in RAM, we can create a handle data reference and pass it to NewM0vieFromOataRef. In this case, the block of m e m o r y referenced by the handle must contain the movie data and the movie atom. To create a movie whose data is stored in RAM, we could use NewTrackMedia as illustrated in the previous section, passing it a handle data reference instead of a file data reference. Or, even more simply, we can exploit the ability of the FlattenMovieOata function to flatten a movie into a location specified by a data reference instead of by a file system specification record. The third parameter to F1 attenMovieData is declared as a pointer to an FSSpec record, but we can set the flag flattenFSSpecPtrIsOataRefRecordPtr to instruct it to interpret that parameter as a pointer to a data reference record, defined by the DataReferenceRecord data type:
The Handle Data Handler 255
struct DataReferenceRecord { OSType Handle
}~
dataRefType; dataRef;
So all we need to do is create a handle data reference, fill in a data reference record with the appropriate information, and then pass the address of that record to FlattenMovieData in place of the FSSpecPtr. Listing 9.5 shows an excerpt from our OTApp_Hand]eMenu function that handles the Create RAM Copy and Play m e n u item. Listing 9.5 Creating a movie in RAM. case IDM CREATE RAM COPY AND PLAY: i f (myMovie l= NULL) { Movie myNewMovie = NULL; Handle myDataRef = NULL; Handle myHandle = NULL; DataRefe ren ceRecord myDataRe fReco rd; myHandle = NewHandleClear(O) ; i f (myHandle == NULL) goto bai] ; myDataRef = QTDR_MakeHandleDataRef(myHandl e) ; i f (myDataRef == NULL) goto bai] ; myDataRefRecord.dataRefType = HandleDataHandl erSubType; myDataRefRecord.dataRef = myDataRef; myNewMovie = Fl attenMovi eData (myMovie, fl attenFSSpecPtr I sDataRefRecordPt r, ( FSSpec Pt r) &myDataRefReco rd, sigMoviePl ayer, smSystemScri pt,
OL);
i f (myNewMovie ! = NULL) { QTDR_Pl ayMovi eFromRAM(myNewMovi e) ; Di sposeMovi e (myNewMovie) ;
}
bail: i f (myHandle ! = NULL) Di sposeHandl e (myHandle) ;
256
Chapter 9 Somewhere I'll Find You
i f (myDataRef l= NULL) Di sposeHandl e (myDataRef) ;
In this case, once w e ' v e created the movie in RAM, w e call the function QTDR_PlayMovieFromRAM to play the movie in a w i n d o w on the screen. T h e n we dispose of the n e w movie and the handle that contains the movie data, along w i t h the h a n d l e data reference.
The Resource Data Handler The resource data h a n d l e r was i n t r o d u c e d in Q u i c k T i m e version 2.5 to allow data to be read from a resource in a file's resource fork. Listing 9.6 shows how to create a resource data reference. Listing 9.6 Creating a resource data reference. Handle QTDR MakeResourceDataRef (FSSpecPtr theFile, OSType theResType, SInt16 theResID)
{
Handle OSType SInt16 OSErr
myDataRef = NULL; myResType; myResID; myErr = noErr;
myDataRef = QTDR_MakeFileDataRef(theFi le) ; i f (myDataRef == NULL) goto bai I ; / / append the resource type and ID to the data reference myResType = EndianU32 NtoB(theResType); myResID = EndianS16 NtoB(theResID); myErr = PtrAndHand(&myResType, myDataRef, sizeof(myResType)) ; i f (myErr == noErr) myErr = PtrAndHand(&myResID, myDataRef, sizeof(myResID)) ; bail 9 i f (myErr l : noErr) { i f (myDataRef l: NULL) Di sposeHandl e (myDataRef) ; myDataRef = NULL;
}
return (myDataRef) ;
The Resource Data Handler
257
In QTDR_MakeResourceDataRef, we begin by creating a file data reference to the specified file. Then we call PtrAndHand to append the resource type to the referring data of that data reference, and then we call PtrAndHand once again to append the resource ID. Note that the resource type and ID must be converted to big-endian format before being appended to the referring data. We w o n ' t spend too m u c h time considering the resource data handler, largely because we prefer not to keep our movie or image data in resource files (so that it's easily transportable to non-Macintosh operating systems). To show you that it works on Macintosh resource files, I've added the m e n u item Open Resource Movie to the QTDataRef sample application; I've also included the file ResBased.mov, which contains a movie stored in a resource. There is another reason w h y the resource data handler is of limited interest to us" it can read data from resource files but cannot write data to them. To see this, let's suppose that myDataRef is a resource data reference. Then we can call the GetDataHandler function like this: myComponent = GetDataHandler(myDataRef, ResourceDataHandl erSubType, kDataHCanWrite) ;
GetDataHandler returns the best data handler for the specified data reference and data handler type that provides the services specified by the third parameter. In this case, we're asking for a resource data handler that can write data. After this line of code completes, however, the value r e t u r n e d in myComponent will be NULL, indicating that there is no such resource data handler. The last reason we're going to ignore the resource data handler in the future is perhaps the most obvious: if we do want to access some movie or image data stored in a resource, we can simply load that data into m e m o r y (by calling GetResource) and then use the handle data handler. So the resource data handler is largely redundant.
T h e URL D a t a H a n d l e r We can use QuickTime to open movies and images that are specified using URLs. A URL is the address of some resource on the Internet or on a local disk. Typically, a URL is a string of characters like "http://www.apple.com." The initial portion of the URL, which precedes the first colon (:), is the URL's scheme or protocol. In this case, the scheme is "http" for the hypertext transfer protocol. QuickTime provides data handlers that can w o r k with URLs whose scheme is "http," "ftp" (file transfer protocol), "rtsp" (real-time
258
Chapter9 Somewhere I'll Find You
streaming protocol), or " f i l e " (picking out a file on the local file system). Note that there really isn't just one URL data handler; there are in fact several data handlers that support URLs. For instance, the file data handler will ultimately be used to handle URLs whose scheme is "file." Still, we access these data handlers using URL data references. As we've already seen, a data reference for the URL data handler is a handle to the NULL-terminated string of characters that comprise a URL. So it's relatively easy to create a URL data reference" just allocate a relocatable block of the appropriate size and copy the URL into that block. Listing 9.7 defines the function QTDR MakeURLDataRef, which creates a URL data reference for a given URL. Listing 9.7 Creating a URL data reference. Handle QTDR_MakeURLDataRef (char *theURL)
{
Handle Size
myDataRef = NULL; mySize = O;
/ / get the size of the URL, plus the terminating null byte mySize = (Size)strlen(theURL) + 1; i f (mySize == 1) goto bai I ; / / allocate a new handle and copy the URL into the handle myDataRef = NewHandleClear(mySize) ; i f (myDataRef ! = NULL) BlockMove(theURL, *myDataRef, mySize) ; bail 9 return (myDataRef) ;
}
If a URL picks out a movie file, we can call QTDR_MakeURLDataRefto create a URL data reference for it and then pass that data reference to the NewMovieFromDataRef function to open the specified movie file. Listing 9.8 defines the function QTURL_NewMovieFromURLthat takes a URL and returns a movie identifier for the movie addressed by that URL. QTDRGetMovieFromURL is just like QTDR GetMovieFromFi l e, except that it uses URL data references.
The URL Data Handler 2 5 9
Listing 9.8 Opening a movie specified by a URL. Movie QTDR_GetMovieFromURL (char *theURL)
{
Movie Handle
myMovie = NULL; myDataRef= NULL;
myDataRef = QTDR_MakeURLDataRef(theURL); i f (myDataRef != NULL) { NewMovieFromDataRef(&myMovie, newMovieActive, NULL, myDataRef, URLDataHandlerSubType); Di sposeHandl e (myDataRef) ;
}
return (myMovie) ;
The QTDR_GetMovieFromURL function is a large part of what we need to handle the Open URL m e n u item (supported both by QuickTime Player and by our sample application QTDataRef). But it isn't quite all of what we need. First, of course, w h e n the user selects Open URL, we need to obtain a URL from him or her. QuickTime Player displays the dialog box shown in Figure 9.3, which contains space to type in a URL, as well as a pop-up m e n u containing a list of recently opened URLs. For the moment, we'll be content to display the dialog box shown in Figure 9.4, which does not provide the pop-up menu. (Providing a pop-up m e n u or some other kind of list for recently accessed URLs is a good idea, however, since URLs are often longer than 255 characters, which is the most our current code will support.) The code to display and manage this dialog box is contained in the function QTDR_GetURLFromUser, which we w o n ' t consider in detail here; it's really quite similar to the function QTInfo_EditAnnotation, which we considered in
Figure 9.3 The Open URL dialog box of QuickTime Player.
260
Chapter 9 Somewhere I'll Find You
Figure 9.4 The Open URL dialog box of QTDataRef.
Chapter 7, "The Informant." It's also useful to get the basename of the URL so that our movie window has a title {the basename is the portion of the URL following the rightmost URL separator). See the file QTOataRef.c for the function QTOR_fietURLBasename, which we use to find that basename.
The Pointer Data Handler The pointer data handler can be used to read data from a location in memory. While the handle data handler references memory using a Memory Manager Handle data type, the pointer data handler references memory using a memory address and a byte count. The memory address can be specified using a Memory Manager Ptr data type, but it does not have to be. In other words, the block of memory specified using a pointer data reference does not need to be a block that was allocated using the Memory Manager. A pointer data reference is a handle to a pointer data reference record, which is defined by the PointerDataRefRecord data type: struct PointerDataRefRecord { void *data; Size dataLength;
};
typedef PointerDataRefRecord *PointerDataRefPtr; typedef PointerDataRefPtr *PointerDataRef;
The Pointer Data Handler 261
The data field is the address of the first byte of a block of data, and dataLength is the length of that block of data. Listing 9.9 shows how to create a pointer data reference.
Listing 9.9 Creating a pointer data reference. Handle QTDR_MakePointerDataRef (void *thePtr, Size theLength)
{
PointerDataRef
myDataRef = NULL;
myDataRef = (PointerDataRef)NewHandleClear(sizeof(PointerDataRefRecord)) ; i f (myDataRef ! = NULL) { (**myDataRef).data = thePtr; (**myDataRef).dataLength = theLength;
}
return( (Handl e)myDataRef) ;
File Transfer We've learned that QuickTime supplies a data handler that can read data from remote locations specified by a URL and a data handler that can write data into files on the local file system. We can use both of these data handlers at the same time to read data stored remotely on the Internet and write it into a local file. This gives us, in effect, a network file transfer capability that operates using only QuickTime APIs. Moreover, QuickTime supports calling these data handlers asynchronously so that our application can perform other processing while the file transfer is taking place. In this section, we'll see how to do all this. Our ulterior motive here is to get a glimpse of the lower-level functions supported by data handlers. Hitherto, we've pretty much always used data handlers indirectly, by passing data references to Movie Toolbox or ICM functions, or simply by embedding data references in files. Now we want to see how to work with data handlers directly. In overview, our file transfer will proceed like this: get an instance of the URL data handler and configure it to copy data from a remote file into a memory buffer. Then get an instance of the file data handler and configure it to copy data from that buffer into a local file. Then keep copying data into and out of the buffer until the entire remote file has been copied. Our file transfer code is going to rely on a few global variables, declared like this:
262
Chapter 9 Somewhere I'll Find You
Ptr Component Ins tance ComponentInstance DataHComplet i onUPP DataHCompl et i onUPP long long Boolean
gDataBuffer = NULL; gDataReader = NULL; gDataWri ter = NULL; gReadDataHCompletionUPP = NULL; gWriteDataHCompletionUPP = NULL; gBytesToTransfer = OL; gBytesTransferred = OL; gDoneTransferring = false;
The variables gDataReader and gDataWri t e r are the component instances of the URL data handler that reads data and the file data handler that writes data. The variable gDataReader puts data into the buffer gDataBuffer, w h e n c e gDataWriter gets the data that it writes into the local file. The asynchronous nature of the file transfer is achieved largely by two completion functions, to which gReadDataHCompleti onUPP and gWri teDataHCompl eti onUPP are universal procedure pointers. Finally, the global variables gBytesToTransfer, gBytesTransferred, and gDoneTransferring keep track of information while the transfer is under way.
Creating the Local File The first thing we need to do, of course, is create the local file into which the remote file data is to be copied. Let's suppose that theFile is a file system specification for the local file (which we perhaps got from the user by calling our framework function QTFrame_PutFi l e). We should first delete that file, if it already exists, by calling FSpDelete: FSpDelete(theFi I e) ;
If the specified file doesn't exist, FSpDelete will return the error code fnfErr, w h i c h we can safely ignore. Then we can call FSpCreate to create a new empty file, like this: myErr : FSpCreate(theFile, kTransFileCreator, kTransFileType, smSystemScri pt) ;
[Note that we are hard-coding the file's type and creator codes here; I'll leave it as an exercise for the reader to concoct a more flexible way to set these values.)
File Transfer 2 6 3
Opening and Configuring Data Handlers Before we can w o r k with a data handler, we need to open an instance of the appropriate data handler component. We saw earlier that we can find a particular data handler by calling GetDataHandler, passing it a data reference, a type, and a set of flags that indicate the data-handling services we need it to provide. If theURL is a C string that specifies the remote file and t h e F i l e is a pointer to a file system specification for the local file into which the remote file data is to be copied, we can create the required data references like this: Handle myReaderRef = QTDR_MakeURLDataRef(theURL) ; Handle myWriterRef = QTDR_MakeFileDataRef(theFi le) ;
And we can open the appropriate data handlers with this code: gDataReader = OpenComponent(GetDataHandl er(myReaderRef, URLDataHandlerSubType, kDataHCanRead)) ; gDataWri ter = OpenComponent(GetDataHandl er(myWri terRef, rAl i asType, kDataHCanWrite) ) ;
As you can see, w e ' r e asking for a URL data handler that can read data and a file data handler that can write data. Once w e ' v e got our data handler instances, we need to configure them by telling t h e m which data references they are going to w o r k with. We also need to have t h e m open a connection to the targets of those data references. We can set the data references by calling DataHSetDataRef, like this: myErr = DataHSetDataRef(gDataReader, myReaderRef) ; myErr = DataHSetDataRef(gDataWriter, myWriterRef) ;
Then we can open the appropriate connections by calling DataH0penForRead and DataHOpenForWri te: myErr = DataHOpenForRead(gDataReader) ; myErr = DataHOpenForWrite(gDataWriter) ;
Transferring Data Synchronously It might seem like we haven't done m u c h work yet, but (believe it or not) we are ready to begin transferring data from the remote file into the local file. At this point, we have a choice: we can transfer the data synchronously (that is, waiting for all the data to arrive before continuing with any other work) or asynchronously. Ultimately, we want to have our transfers proceed asynchronously, but let's take a m o m e n t to see how to do t h e m synchronously.
264
Chapter9 Somewhere I'll Find You
To perform a synchronous transfer, we can call DataHGetData and DataHPutData. These functions require the intermediate buffer to be accessed by a handle (not by a pointer, as with asynchronous transfers). We can call DataHGetFileSize to see how m a n y bytes we need to transfer and then allocate a handle of that size: myErr = DataHGetFileSize(gDataReader, &gBytesToTransfer) ; Handle gDataBuffer = NewHandleClear(gBytesToTransfer);
If gDataBuffer is successfully created, we can transfer the remote file to the local file with these four lines of code: DataHGetData(gDataReader, gDataBuffer, OL, OL, gBytesToTransfer); DataHCl oseForRead(gDataReader) ; DataHPutData(gDataWriter, gDataBuffer, OL, NULL, gBytesToTransfer); DataHCl oseForWri te (gDataWri ter) ;
All we need to do is fetch the remote data into the intermediate buffer, close the connection to the remote file, write the data into the local file, and then close the connection to the local file. So there you have it: with barely a dozen lines of code, w e ' v e managed to copy a file located somewhere out on the Internet into a local file. The downside here is that the transfer occurs synchronously and is limited to files that can fit entirely into RAM. Let's remove these two limitations.
Transferring Data Asynchronously We can remove the limitation on file size rather simply, by allocating an intermediate buffer of a set size and then reading and writing chunks of data of that size. We'll allocate a buffer that is 10 Kbytes, like this: #define kDataBufferSize
1024"10
ptr gDataBuffer = NewPtrClear(kDataBufferSize);
So all we need to do is read a c h u n k of data of size kDataBufferSize, write that chunk of data, read another c h u n k of data, write that chunk, and so on until the entire remote file is transferred. Of course, eventually we'll probably end up with a c h u n k that's smaller than kOataBufferSize, so we'll need to keep track of how m u c h of the file remains to be transferred and adjust our reads accordingly. We can achieve an asynchronous transfer by using the functions DataHReadAsync and DataHWrite to read and write data. These functions queue up a read or write request to the data handler and then return immediately,
File Transfer 265
without waiting for the request to be serviced. The request will be serviced at some later time, when we call DataHTask to task the data handler. Once the request is serviced, the data handler executes a data handler completion function that we specify when we call DataHReadAsync or DataHWrite. A data handler completion function takes three parameters: a pointer to the buffer into which data was written or from which data was read, an application-specific reference constant, and an error code. We'll ignore the error code and use the reference constant to hold the number of bytes just written or read by the data handler. We begin the file transfer by reading some data from the remote file. We could simply call OataHReadAsync directly, passing it the appropriate parameters. Instead, however, we'll be a bit clever here and call our write completion function QTDR_WriteDataCompletionProc, passing it parameters that indicate that we've just successfully finished writing 0 bytes" QTDR_WriteDataCompletionProc(gDataBuffer, OL, noErr); The QTDR_WriteDataCompletionProc function (defined in Listing 9.10) contains code for figuring out how many bytes to request and then for issuing
the appropriate read request. Listing 9.10 Responding to a write operation. PASCAL_RTN voi d QTDR_WriteDataCompl et i onProc (Ptr theRequest, long theRefCon, OSErr theErr)
{
#pragma unused(theErr) long myNumBytesToRead; wide myWide; / / increment our t a l l y of the number of bytes written so far gBytesTransferred += theRefCon; i f (gBytesTransferred < gBytesToTransfer) { / / there is s t i l l data to read and write, so schedule a read operation / / determine how big i f (gBytesToTransfer myNumBytesToRead = else myNumBytesToRead =
a chunk to read - gBytesTransferred > kDataBufferSize) kDataBufferSize; gBytesToTransfer - gBytesTransferred;
myWide, lo = gBytesTransferred; myWide.hi = O;
R66
Chapter 9 Somewhere I'll Find You
/ / read from the current offset
/ / schedule a read operation Dat aHReadAsync ( gDat aReader, theRequest, myNumBytesToRead, &myWide, gReadDataHCompIet i onUPP, myNumBytesToRead) ;
/ / the data buffer
} else { / / we've transferred all the data gDoneTransferring = true;
}
As you can see, we first update the global variable gBytesTransferred that keeps track of the n u m b e r of bytes already transferred. Then we figure out how m a n y bytes remain to be read from the remote file; we read that number of bytes, if it's less than the size of our intermediate buffer, or else we read an entire buffer of data. Finally, we call DataHReadAsync to schedule a read operation. Notice that we specify the gReadDataHCompletionUPP as the read completion function. Once some data is read into the local buffer, our read completion function (Listing 9.11)will be executed. It's even simpler than the write completion function; all it does is schedule a write operation to copy the data from the buffer into the local file.
Listing 9.11
Responding to a read operation.
PASCAL_RTN void QTDR_ReadDataCompletionProc (Ptr theRequest, long theRefCon, OSErr theErr)
{
#pragma unused(theErr) / / we just finished reading some data, DataHWri te (gDataWri ter, theRequest, gBytesTransferred, theRefCon, gWri teDataHComplet i onUPP, theRefCon) ;
so schedule a write operation / / the data buffer / / write from the current offset / / the number of bytes to write
In this case, theRefCon contains the n u m b e r of bytes just read from the remote file, which is the n u m b e r of bytes that should be written to the local
File Transfer
267
file. Notice that now we specify gWriteDataHCompletionUPP as the write completion function. The read and write completion functions keep scheduling write and read requests, specifying each other as the completion function for those requests. So we keep successively reading and writing data, until the entire file is transferred.
Tasking the Data Handlers There is one final step needed to make this all work. Namely, we need to give the data handlers some processor time to do their work. We do this by periodically calling DataHTask. On the Macintosh, we can insert calls to DataHTask into the application function QTApp_Hand]eEvent, which is called by our application framework every trip through the main event loop. Listing 9.12 shows the definition of QTApp_Hand]eEvent in QTDataRef. Listing 9.12 Taskingthe data handlers. Boolean QTApp_HandleEvent (EventRecord *theEvent)
{
#pragma unused(theEvent) / / i f we're done, close down the data handlers i f (gDoneTransferring) QTDR_CloseDownHandlers () ; / / give the data handlers some time, i f they are s t i l l active i f (gDataReader != NULL) DataHTask(gDataReader) ; i f (gDataWriter l= NULL) DataHTask(gDataWri ter) ; return(false);
When the file is done being transferred, then QTApp_HandleEvent calls the function QTDR_CloseDownHandlers (defined later) to close things down. Otherwise, it calls DataHTask on both the URL data handler and the file data handler. At most one of them will have some work to do, but it doesn't hurt to task both of them. Our Windows framework does not, however, call QTApp_HandleEvent periodically, so we'll have to do that ourselves. Probably the easiest way is to install a timer task, like this" gTimerlD : SetTimer(NULL, O, kQTDR_TimeOut, (TIMERPROC)QTDR_TimerProc);
268
Chapter9 Somewhere I'll Find You
The timer callback function QTDR_TimerProc, defined in Listing 9.13, simply calls QTApp_HandleEvent.
Listing 9.13 Handling timer callbacks. void CALLBACKQTDR_TimerProc (HWNDtheWnd, UINT theMessage, UINT_PTR theID, DWORDtheTime)
{
#pragma unused(theWnd, theMessage, theID, theTime) QTApp_HandleEvent(NULL);
}
We need to remove the timer task once the data transfer is completed (as accomplished in Listing 9.14). It's important to know that a data handler might not execute our requests to read and write data asynchronously, even if we specify completion functions. If the handler decides to operate synchronously, then it will not return immediately when we call DataHReadAsync or DataHWrite; instead, it will perform the requested operation and then return. We still need to call DataHTask, however, to give the data handlers an opportunity to execute their completion functions.
Finishing Up When the write completion function QTDR_WriteDataCompletionProc determines that all the data has been read from the remote file and written into the local file, it sets the global variable gDoneTransferring to true. When OTApp_HandleEvent is called and gDoneTransferring is true, QTApp_HandleEvent calls QTDR_CloseDownHandlers, defined in Listing 9.14. Listing 9.14 Closing down the data handlers.
void QTDR_CloseDownHandlers (void)
(
i f (gDataReader l= NULL) { DataHCloseForRead(gDataReader) ; Cl oseComponent(gDataReader) ; gDataReader = NULL;
}
i f (gDataWriter l: NULL) { DataHCloseForWri te (gDataWri ter) ; Cl oseComponent(gDataWri ter) ; gDataWri ter = NULL;
}
File Transfer 269
/ / dispose of the data buffer i f (gDataBuffer != NULL) Di sposePtr (gDataBuffer) ; / / dispose of the routine descriptors i f (gReadDataHCompletionUPP ! = NULL) Di sposeDataHCompI et i onUPP(gReadDataHCompIet i onUPP) ; i f (gWriteDataHCompletionUPP ! = NULL) Di sposeDataHCompleti onUPP(gWri teDataHComp]eti onUPP) ; gDoneTransferri ng = false; #if TARGETOS WIN32 / / k i l l the timer that tasks the data handlers KilITimer(NULL, gTimerID) ; #endi f B
}
QTDR_CloseDownHandlers simply doses the connections to the ]oca| and
remote files and then closes the component instances On Windows, it also removes the timer task that was Event periodically. Listing 9.15 contains the complete definition of the ToLoca] Fi ] e function, which is called in response to the m e n u item.
Listing
9.15
os the data handlers. calling QTApp_HandleQTOR_CopyRemoteFileTransfer Remote File
Copying a remote file into a local file.
OSErr QTDR_CopyRemoteFileToLocalFile (char *theURL, FSSpecPtr theFile)
(
Handle Handle ComponentResult
myReaderRef = NULL; myWriterRef = NULL; myErr = badComponentType;
/ / data ref for the remote f i l e / / data ref for the local f i l e
/ / delete the target local f i l e , i f i t already exists; / / i f i t doesn't exist yet, we'll get an error (fnfErr), which we just ignore FSpDelete (theFi le) ; / / create the local f i l e with the desired type and creator myErr = FSpCreate(theFile, kTransFileCreator, kTransFileType, smSystemScript); i f (myErr ! = noErr) goto bai I ;
270
Chapter 9 Somewhere I'll Find You
/ / create data references for the remote f i l e and the local f i l e myReaderRef = QTDR_MakeURLDataRef(theURL) ; i f (myReaderRef == NULL) goto bai I ; myWriterRef = QTDR_MakeFileDataRef(theFi le) ; i f (myWriterRef == NULL) goto bai I ; / / find and open the URL and f i l e data handlers gDataReader = OpenComponent(GetDataHandl er(myReaderRef, URLDataHandl erSubType, kDataHCanRead)) ; i f (gDataReader == NULL) goto bai I ; gDataWri ter = OpenComponent(GetDataHandl er(myWri terRef, rAl i asType, kDataHCanWrite)) ; i f (gDataWriter == NULL) goto bai I ; / / set the data reference for the URL data handler myErr = DataHSetDataRef(gDataReader, myReaderRef) ; i f (myErr ! = noErr) goto bai I ; / / set the data reference for the f i l e data handler myErr = DataHSetDataRef(gDataWriter, myWriterRef) ; i f (myErr I= noErr) goto bai I ; / / allocate a data buffer; the URL data handler copies data into this buffer, / / and the f i l e data handler copies data out of i t gDataBuffer = NewPtrClear(kDataBufferSize) ; myErr = MemError() ; i f (myErr ! = noErr) goto bai I ; / / open a read-only path to the remote data reference myErr = DataHOpenForRead(gDataReader) ; i f (myErr ! = noErr) goto bai I ; / / get the size of the remote f i l e myErr = DataHGetFileSize(gDataReader, &gBytesToTransfer) ; i f (myErr l= noErr) goto bai I ;
File Transfer
271
/ / open a write-only path to the local data reference myErr = DataHOpenForWrite(gDataWriter) ; i f (myErr l= noErr) goto bai I ; / / start reading and writing data gDoneTransferring = fal se; gBytesTransferred = OL; gReadDat aHCompIet i onUPP = NewDataHCompI et i onUPP(QTDR_ReadDataCompIet i onProc) ; gWri teDataHCompI et i onUPP = NewDataHCompIet i onUPP(QTDR_WriteDataCompIet i onProc) ; / / start retrieving the data; we do this by calling our own write completion routine, / / pretending that we've just successfully finished writing 0 bytes of data QTDR_WriteDataCompl eti onProc (gDataBuffer, OL, noErr) ; bail: / / i f we encountered any error, close the data handler components i f (myErr I= noErr) QTDR_CloseDownHandl ers () ; return((OSErr)myErr) ;
So we've managed to use QuickTime's data handlers to provide a generalpurpose network file transfer capability that operates asynchronously, allowing us to play movies or perform other operations while the transfer is under way. Of course, there are still some refinements we might add, such as alerting the user if he or she decides to quit the application while a file transfer is in progress. I'll leave these as exercises for the interested reader. Notice that we call the function DataHGetFileSize to determine the size of the remote file (which is, of course, the number of bytes we need to transfer). DataHGetFileSize may need to read through the entire remote file to determine its size, which can sometimes slow things down (since we're calling it synchronously). Some data handlers (but not all) support the DataHGetFileSizeAsync function, which allows us to get this information asynchronously. You might try experimenting with DataHGetFileSizeAsync to see if it improves performance in your particular situation. In that vein, you might be wondering: Sure, we can use QuickTime to transfer data across the net, but is it any good? What's the performance like? My preliminary (and admittedly unscientific) tests show that our code is in fact very good. In a few sample FTP transfers, QTDataRef consistently transferred files at least as fast as the latest version of a popular shareware
272
Chapter9 Somewhere I'll Find You
Internet file transfer application for the Mac. Moreover, with a movie playing continuously in the foreground, QTDataRef took only about 10% longer to transfer the file than it took with no movie playing. And don't forget that our code works on Mac OS 8 and 9, Windows, and Mac OS X!
D a t a R e f e r e n c e Extensions Consider now this question" if we pass a handle data reference to the function GetGraphicsImporterForOataRef, how does it figure out which graphics importer to open and return to us? Recall (from Chapter 4, "The Image") that when we pass a file specification record to GetGraphicsImporterForFile, it first inspects the Macintosh file type (on Mac OS) and then the filename extension of the specified file. If neither of these inspections reveals the type of image data in the file, GetGraphicsImporterForFile must then validate the file data (that is, look through the file data for clues to the image type). With a handle data reference, where there is no file type or fllename extension, only the validation step is possible. Unfortunately, validation is timeconsuming and, alas, not guaranteed to produce correct results. QuickTime 3.0 provided a preliminary solution to this problem by allowing us to attach a filename to the referring data of a handle data reference. (Let's call this a I~lenaming extension.} That is to say, a handle data reference is a handle to a 4-byte handle that is optionally followed by a Pascal string containing a fllename. Listing 9.16 defines the function QTDR_AddFilenamingExtension that attaches a filename to the referring data of a handle data reference. Listing 9.16 Appending a filename to some referring data. OSErr QTDR_AddFilenamingExtension (Handle theDataRef, StringPtr theFileName)
{
unsigned char OSErr
myChar = O; myErr = noErr;
i f (theFileName == NULL) myErr = PtrAndHand(&myChar, theDataRef, sizeof(myChar)) ; else myErr = PtrAndHand(theFileName, theDataRef, theFileName[O] + 1); return (myErr) ;
Data Reference Extensions 2 7 3
The filename can contain an extension that provides an indication of the kind of data that's in the data reference target. For instance, a filename of the form myImage.bmp indicates that the data consists of Windows bitmap data. For reasons that will become clear in a few moments, QTDR_AddFi]enamingExtension looks to see w h e t h e r theFileName parameter is NULL; if it is, QTDR_AddFi 1enami ngExtensi on appends a single byte whose value is O. QuickTime 4.0 provides a more complete solution to this problem by allowing us to create data reference extensions for handle data references. A data reference extension is a block of data that is appended to the referring data, in pretty m u c h the same way that we just appended a fllename to that data. The main difference is that, unlike the filenaming extension, a data reference extension is packaged as an atom, with an explicit type. QuickTime currently supports four kinds of data reference extensions, defined by these constants" enum { kDataRefExtensionChokeSpeed kDataRefExtensionMIMEType kDataRefExtensionMacOSFileType kDataRefExtensionlnitializationData
};
= = = =
FOURCHARCODE('chok'), FOURCHARCODE('mime'), FOURCHARCODE('ftyp'), FOURCHARCODE('data') n
A data reference extension of type kDataRefExtensionChokeSpeed can be
added to a URL data reference to specify a choke speed (which limits the data rate of a file streamed using HTTP streaming). The other three types can be added to a handle data reference to help identify the kind of data in the target of the data reference or to supply some initialization data to the data handler. If a data reference extension is present, then the filenaming extension must also be present. The filename can be O-length, however, in which case the filenaming extension consists only of a single byte whose value is O. Listing 9.17 shows how to add a Macintosh file type as a data reference extension.
Listing 9.17 Appending a file type data reference extension. OSErr QTDR_AddMacOSFileTypeDataRefExtension (Handle theDataRef, OSType theType)
{
unsigned l o n g OSType OSErr
myAtomHeader[2] ; myType; myErr = noErr;
myAtomHeader[O] : EndianU32 NtoB(sizeof(myAtomHeader) + sizeof(theType)); myAtomHeader[1] = EndianU32 NtoB(kDataRefExtensionMacOSFileType); D
274
Chapter 9 Somewhere I'll Find You
myType = EndianU32 NtoB(theType) ; m
myErr = PtrAndHand(myAtomHeader, theDataRef, sizeof(myAtomHeader)) ; i f (myErr == noErr) myErr = PtrAndHand(&myType, theDataRef, sizeof(myType)) ; return (myErr) ;
This code simply calls PtrAndHand to append an atom header to the referring data and then calls PtrAndHand again to append the file type (suitably converted to big-endian format). Another way to flag the type of data in a handle is by specifying a MIME type. MIME (for Multipurpose Internet Mail Extension) is a standard protocol for transmitting binary data across the Internet. A MIME type is a text string used in MIME transmissions to indicate the type of the data being transmitted. (For instance, the string "video/quicktime" is the MIME type of QuickTime movie files.) MIME types can also be used locally to indicate the type of a file or other collection of data. Movie importers and graphics importers will look for MIME type data reference extensions to help identify the type of data specified by a handle data reference. If you are building some movie or image data in memory, you can use the QTDR_AddMIMETypeDataRefExtension function, defined in Listing 9.18, to add a MIME type as a data reference extension. (Be sure to add a filenaming extension before adding a MIME type data reference extension.) Listing 9.18 Appending a MIME type data reference extension. OSErr QTDR_AddMIMETypeDataRefExtension (Handle theDataRef, StringPtr theMIMEType)
{
unsigned long OSErr if
myAtomHeader[2] ; myErr = noErr;
(theMIMEType == NULL) return (paramErr) ;
myAtomHeader[O] = EndianU32 NtoB(sizeof(myAtomHeader) + theMIMEType[O] + 1); myAtomHeader[1] = EndianU32 NtoB(kDataRefExtensionMIMEType) ; B
myErr = PtrAndHand(myAtomHeader, theDataRef, sizeof(myAtomHeader)) ; i f (myErr == noErr) myErr = PtrAndHand(theMIMEType, theDataRef, theMIMEType[O] + 1); return (myErr) ;
Data Reference Extensions 275
Conclusion As we've seen, QuickTime uses data references to find the data that it's supposed to handle. Data references can be e m b e d d e d in movie files or passed to Movie Toolbox and ICM functions. So w h e t h e r we're building movies or operating on them, understanding data references is crucial to doing any real work with QuickTime. Here we've learned how to w o r k with data references to create reference movie files, play movies from RAM, and open movies located remotely on the Internet. In the previous chapter, we also saw how to use data references to create shortcut movie files and e m b e d d e d movies. In future chapters, we'll work with data references as a normal part of our QuickTime programming. On the other hand, data handlers are normally transparent to applications. We can use them directly for certain special purposes, such as transferring remote files to the local machine. But normally the QuickTime APIs insulate us from having to work with them at all. Typically, we can accomplish what we need by handing the Movie Toolbox or ICM a data reference and letting it communicate with the appropriate data handler.
276
Chapter9 Somewhere I'll Find You
W o r d Is Out
Introduction W h e n 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 10.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 dialog 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. The text you see in Figure 10.1 is not part of the video track; rather, it is stored in a text track (whose associated media is of type TextMedi aType). Typically the text track is situated below the video track (as in Figure 10.1), but in fact it can overlay part or all of the video track. In order for both the text and the underlying video to be visible, the background of the text track should be transparent or "keyed out"; the text is then called keyed text. Figure 10.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. 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); w h e n a movie contains a chapter track, the movie controller will build, display, and handle a pop-up m e n u that contains the text in the various samples in that track. The pop-up
277
Figure 10.1
A movie containing a text track.
Figure 10.2 A movie containing a keyed text track.
278
Chapter 10 Word Is Out
menu appears (space permitting) in the controller bar. The various parts of the associated track are called the track's chapters. When the user chooses an item in the pop-up menu, the movie controller jumps to the start time of the selected chapter. Figure 10.3 shows our standard appearing-penguin movie with a chapter track that indicates the percentage of completion {both before and after the user clicks the pop-up menu). Notice that we've had to hide the step buttons in the controller bar to make room for the chapter popup menu. Notice also that the text track itself is not visible. 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 10.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. 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
,the QuickTime plug-in will load the specified URL in the frame containing the movie when the user clicks the movie box while that text sample is active. {Let's call this a clickable link.) If the text is in the form A,then the plug-in will load the specified URL automatically when that text sample becomes active. (Let's call this an
automatic link.)
Figure 10.3 A movie with a chapter track.
Introduction
279
Figure 10.4 The chapter controls in a QuickTime Player movie window.
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 future chapters; for the moment, consider the movie shown in Figure 10.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 h t t p : / / ~w. appl e. corn; in addition, rolling the cursor over the word "Pixar" loads the URL h t t p : / / ~ . p i x a r . c o m / . (Let's call this wired text.) In this chapter, 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
280
Chapter 10 Word Is Out
Figure 10.5 A text track with wired actions.
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 chapter, 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 10.6 shows the Test menu of this chapter's sample application, named QTText.
T h e Edit M e n u 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 the menu bar to choose one of our application's menus, our application framework calls the OTFrame_AdjustMenus function. (In our Macintosh framework, this happens in response to a mouseDown event in the
The Edit Menu Revisited 281
Figure 10.6
The Test menu of QTText. i nMenuBar window part; in our Windows framework, this happens when the MDI frame window receives the WM_INITMENUcommand.) Listing 10.1 shows the code in QTFrame_AdjustMenus that adjusts the Edit menu.
Listing 10.1
Adjusting the Edit menu (original version).
# i f TARGET OS MAC myMenu = GetMenuHandle (kEdi tMenuResID) ; #endif i f (myMC ! = NULL) { long myFlags; I
I
MCGetControl lerlnfo(myMC, &myFlags) ; QTFrame_SetMenultemState(myMenu, IDM_EDITUNDO, myFlags & mclnfoUndoAvailable ? kEnableMenultem : kDisableMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCUT, myFlags & mclnfoCutAvailable ? kEnabl eMenultem 9kDi sabl eMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCOPY, myFlags & mclnfoCopyAvailable ? kEnableMenultem : kDisableMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITPASTE, myFlags & mclnfoPasteAvailable ? kEnableMenultem 9kDisableMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCLEAR, myFlags & mcInfoClearAvailable ? kEnableMenultem 9kDi sableMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITSELECTALL, myFlags & mclnfoEditingEnabled ? kEnabl eMenultem 9kDi sabl eMenultem) ;
282
Chapter 10 Word Is Out
QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE,myFlags & mcInfoEditingEnabled ? kEnableMenultem : kDisableMenultem) ; } else { QTFrame_SetMenultemState(myMenu, IDM_EDITUNDO, kDi sabl eMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCUT, kDi sabl eMenuItem) ; QTFrame_SetMenuItemState(myMenu, IDM_EDITCOPY, kDi sabl eMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITPASTE, kDi sabl eMenuItem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCLEAR, kDi sabl eMenultem) ; QTFrame SetMenultemState(myMenu, IDM EDITSELECTALL, kDisableMenultem) ; QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, kDi sabl eMenultem) ;
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 el se portion). Otherwise, we call the MCfetControllerInfo function to determine the current status of the movie controller and its associated movie. MCfetControllerInfo 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 mclnfoPasteAvailable flag is set in the 32-bit long integer returned by MCGetControl lerInfo. 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 SetMenultemState(myMenu, IDM EDITPASTE, myFlags & mclnfoPasteAvailable ? kEnableMenultem
9kDisableMenultem);
We've already considered QTFrame_SetMenuItemState in Chapter 1, "It All Starts Today"; 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 the Edit menu, we'll see something like the menu shown in Figure 10.7. 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
The Edit Menu Revisited 2 8 3
Figure 10.7 The Edit menu of QuickTime Player (Option key down).
Figure 10.8 The Edit menu of QuickTime Player (Shift key down). the Edit menu, we'll see the menu shown in Figure 10.8. Now the Paste menu item is labeled "Replace." Finally, if we hold down the Option and the Shift keys (or, on Windows, the Ctrl and Alt and Shift keys)while clicking the Edit menu, the Paste menu item will be labeled "Add Scaled," as shown in Figure 10.9. (For the moment, don't worry about what these renamed menu items actually do; we'll get to that in the next section.) What's happening here is that QuickTime Player is not using MCGetControl lerInfo 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: ComponentResul t MCSetUpEditMenu (MovieControl I er mc, 1ong modif i ers, MenuHandle mh) ; MCSetUpEditMenu correctly enables or disables and names the first five com-
mands 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 just described by using MCSetUpEditMenu ourselves.
284
Chapter l O Word ls Out
Figure 10.9 The Edit menu of QuickTime Player (Shift and Option keys down). There are just a couple of changes w e n e e d to m a k e to support MCSetUpEditMenu. Primarily, we n e e d to add a p a r a m e t e r to our QTFrame_AdjustMenus function so that w e can pass it the c u r r e n t k e y b o a r d modifiers. H e n c e f o r t h , QTFrame_AdjustMenus will be declared like this:
int QTFrame_AdjustMenus (WindowReference theWindow, MenuReference theMenu, long theModifiers) ; Getting the appropriate k e y b o a r d modifiers in our M a c i n t o s h code is easy. W h e n e v e r we call QTFrame AdjustMenus, either w e d o n ' t care about the moditiers (so we can pass OL) or we have an event record available (so we can pass
(long) t heEvent->modi fi ers).
Getting the M o d i f i e r Keys on W i n d o w s W h e n we call QTFrame AdjustMenus on W i n d o w s , however, we need to do u
some additional work to determine which (if any) modifier keys the user is holding down when clicking 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 Ctr] 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 shiftKey alphaLock optionKey
};
control Key
= = = =
1 1 1 1
<< << << <<
cmdKeyBit, shiftKeyBit, alphaLockBit, optionKeyBit,
= 1 << control KeyBit
// // // //
Ox0100 Ox0200 Ox0400 Ox0800
/ / OxlO00
The Edit Menu Revisited
285
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: 9 The Windows Alt key is mapped to the Macintosh Control key. 9 The Windows Ctrl key is mapped to the Macintosh Command key. 9 The Windows Shift key is mapped to the Macintosh Shift key. 9 The Windows Caps Lock key is mapped to the Macintosh Caps Lock key. 9 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 #define #define #define
VK MAC CONTROLKEY VK MAC COMMANDKEY VK MAC SHIFTKEY VK MAC CAPSKEY
VK MENU VK CONTROL VK SHIFT VK CAPITAL
#defi ne QTFrame_IsControl KeyDown(theKeyState) (theKeyState[VK_MAC_CONTROLKEY] & Ox80 ? i : O) #def i ne QTFrame_IsCommandKeyDown(t heKeyState) (theKeyState[VK_MAC_COMMANDKEY] & Ox80 ? 1 : O) #define QTFrame_IsShi f t KeyDown(theKeyState) (theKeyState[VK_MAC_SHIFTKEY] & OxSO ? I 9O) #defi ne QTFrame_IsAl phaLockKeyDown(theKeyState) (theKeyState[VK_MAC_CAPSKEY] & Ox80 ? I : O) #def i ne QTFrame_IsOpti onKeyDown( theKeyState) (QTFrame_IsControl KeyDown(theKeyState)) && (QTFrame IsCommandKeyDown(theKeyState))
\ \ \ \ \ \
m
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
286
Chapter 10 Word Is Out
encodes that information. W h e n we need to call OTFrame_AdjustMenus, we can get the current set of modifier keys by calling OTFrame_GetKeyboardModi f i e f s , defined in Listing 10.2.
Listing 10.2. Getting the Windows keyboard modifier keys. static long QTFrame_GetKeyboardModifiers (void)
{
l ong BYTE
myModifiers = OL; myKeyState[256] ;
i f (GetKeyboardState(&myKeyState[O])) { i f (QTFrame IsOptionKeyDown(myKeyState)) myModifiers I = optionKey; el se i f (QTFrame_IsCommandKeyDown(myKeyState)) myModifiers I = cmdKey; el se i f (QTFrame_IsControl KeyDown(myKeyState)) myModifiers I = controIKey; i f (QTFrame IsShiftKeyDown(myKeyState)) myModifiers I = shiftKey; i f (QTFrame_IsAlphaLockKeyDown(myKeyState)) myModifiers I = alphaLock; return (myModifi ers) ;
So, on Windows, we are now able to pass the correct set of modifier flags to MCSetUpEditMenu. But w h a t do we pass for the third parameter, w h i c h on Mac OS is a m e n u 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 m e n u s using a value of type HMENU,not MenuHand] e. This means, however, that on W i n d o w s we cannot d e p e n d on MCSetUpEditMenu to either highlight or r e n a m e the items in the Edit menu. For that, we'll have to write our o w n code.
R e n a m i n g t h e Edit M e n u I t e m s on W i n d o w s At this point, you might be w o n d e r i n g w h y w e ' r e bothering to call MCSetUpEdi tMenu on Windows, if it isn't going to help us with highlighting or renaming the items in the Edit menu. The a n s w e r is that MCSetUpEditMenu does more than simply enable or disable m e n u items and r e n a m e t h e m to m a t c h the state of the active modifier keys. MCSetUpEditMenu also sets some flags
The Edit Menu Revisited 287
maintained internally by the movie controller that affect the operation of subsequent editing commands. For instance, w h e n we call MCPaste, it looks at those flags to determine whether it should paste, replace, 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 10.1 again), so we'll just conditionalize that code to be executed under Windows but not under Mac OS. 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 that 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 myStri ng 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 10.3, handles all of this for us. Listing 10.3
Renaming a Windows Edit menu item.
void QTFrame ConvertMacToWinMenultemLabel (MovieController theMC, MenuReference theWinMenu, long theModifiers, UInt16 theMenultem) Str255 char char char short
myString; *myLabeIText= NULL; *myBeginText= NULL; *myFinaIText = NULL; myLabelSize= O;
/ / get the appropriate label for the specified item and keyboard modifiers MCGetMenuString(theMC, theModifiers, MENU ITEM(theMenultem), myString); switch (theMenultem) { case IDM EDITUNDO: myBeginText = kAmpersandText; myFinaIText = kWinUndoAccelerator; break; case IDM EDITPASTE. myBegi nText = ""; myFinaIText = kWinPasteAccelerator;
288
Chapter 10 Word Is Out
break; case IDM EDITCLEAR: myBeginText = kAmpersandText; myFinaIText = 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[O] + strlen(myFinaIText) + 1; myLabeIText = malloc(myLabeISize) ; i f (myLabeIText == NULL) return; BlockMove(myBeginText, myLabeIText, strlen(myBeginText)) ; BlockMove(&myString[1], myLabeIText + strlen(myBeginText), myString[O]) ; BlockMove(myFinaIText, myLabeIText + strlen(myBeginText) + myString[O], strl en (myFinal Text) ) ; myLabeIText[myLabeISize- 1] = ' \ 0 ' ; QTFrame SetMenultemLabel (theWi nMenu, theMenultem, myLabelText) ; free (myLabelText) ;
QTFrame_ConvertMacToWinMenultemLabel also adds the ampersand (&) to the beginning of several of the Edit m e n u 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 10.4 shows our revised version of Listing 10.1. W h e n no movie controller is available, we disable all the Edit m e n u items in exactly the same m a n n e r we did in our earlier version. And for the Select All and Select None items, we call MCfietControllerlnfo and QTFrame_SetMenultemState, just like before. But for the five standard Edit m e n u commands, we now call MCSetUpEditMenu on both Mac and Windows. In addition, on Windows we need to do all m e n u item enabling and disabling ourselves, and we need to update the m e n u item labels, using our function QTFrame_ConvertMacToWinMenuItemLabel.
The Edit Menu Revisited 2 8 9
Listing 10.4 Adjusting the Edit menu (revised version). # i f TARGET OS MAC myMenu = GetMenuHandle(kEdi tMenuResID) ; #endif i f (myMC == NULL) { / / i f there is no movie control ler, disable all the Edit menu items QTFrame_SetMenuItemState (myMenu , IDM EDITUNDO, kDisableMenultem) ; QTFrame SetMenultemState(myMenu, IDM EDITCUT, kDisableMenultem); QTFrame SetMenultemState(myMenu, IDM EDITCOPY, kDisableMenuItem); QTFrame SetMenultemState(myMenu, IDM EDITPASTE, kDisableMenultem); QTFrame_SetMenuItemSta te (myMenu , IDM EDITCLEAR, kDisableMenultem); QTFrame_SetMenuItemSta te (myMenu , IDM EDITSELECTALL, kDisableMenuItem) ; QTFrame_SetMenuItemSta te (myMenu , IDM EDITSELECTNONE, kDisableMenultem); else { MCGetControl lerlnfo(myMC, &myFlags) ; D
QTFrame_SetMenultemState(myMenu, IDM_EDITSELECTALL, myFlags & mclnfoEditingEnabled ? kEnabl eMenultem 9kDi sabl eMenultem) ; QTFrame_SetMenuItemState(myMenu, IDM_EDITSELECTNONE, myFlags & mcInfoEditingEnabled ? kEnabl eMenuItem 9kDi sabl eMenuItem) ; # i f TARGET OS MAC MCSetUpEditMenu(myMC, theModi fiers, myMenu); #endif # i f TARGET OS WIN32 MCSetUpEditMenu(myMC, theModifi ers, NULL); QTFrame_SetMenultemState(myMenu, IDM_EDITUNDO, myFlags & mclnfoUndoAvailable ? kEnableMenultem : kDisableMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCUT, myFlags & mclnfoCutAvailable ? kEnableMenultem 9kDisableMenuItem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCOPY, myFlags & mclnfoCopyAvailable ? kEnableMenultem : kDisableMenultem) ; QTFrame SetMenultemState(myMenu, IDM EDITPASTE, myFlags & mclnfoPasteAvailable ? kEnableMenultem : kDisableMenultem) ; QTFrame_SetMenultemState(myMenu, IDM_EDITCLEAR, myFlags & mclnfoCl earAvai I abl e ? kEnableMenultem : kDisableMenultem) ; QTFrame_ConvertMacToWi nMenultemLabel (myMC, myMenu, theModifi ers, QTFrame_ConvertMacToWi nMenultemLabel (myMC, myMenu, theModi fiers, QTFrame_ConvertMacToWi nMenultemLabel (myMC, myMenu, theModif i e r s , QTFrame_ConvertMacToWinMenultemLabel (myMC, myMenu, theModifiers, QTFrame_ConvertMacToWinMenultemLabel (myMC, myMenu, theModifiers, #endi f
}
290
Chapter 10 Word Is Out
IDM_EDITUNDO); IDM_EDITCUT); IDM_EDITCOPY); IDM_EDITPASTE); IDM_EDITCLEAR);
There is one final modification that we need to make to our W i n d o w s source code. Apparently, on Windows calling the MCIsP]ayerEvent 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 chapter'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 Chapter 6, "Doug's 1st Movie." 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 c o m m a n d in the Edit m e n u (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). Voile--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 w e ' 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 as summarized in the following list. 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
Text Importing 291
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 c u r r e n t selection; otherwise, if there is no selection, the p a s t e d text replaces the entire movie. In both cases, the duration of the pasted text sample is the default two seconds. 9 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.
9 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 choosing Paste in the Edit menu causes the text movie importer to display the Text Import Settings dialog box, shown in Figure 10.10. This dialog box allows the user to configure some settings of the pasted text.
I m p o r t i n g T e x t f r o m a File The text movie importer can also import text stored in a file and, indeed, provides some additional capabilities that are not available w h e n 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 Chapter 4, "The Image," 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:
292
Chapter 10 Word Is Out
Figure 10.10 The Text Import Settings dialog box. (QTtext} { font :Tekton} {plain} { size: 18} {textColor: O, O, O}{backColor: 65535, 65535, O} {justify:center} {timeScal e:600} {width:240} {height:40} { t i meStamps:absolute } { language: 0} { text Encoding :0 } { shri nkTextBox: on} [00:00:00.000] {textBox: 10, O, 30, 240}We forgot to seed! [00:00:01.000] {textBox: 10, O, 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]
Text Importing
2931
Figure 10.11 The imported movie. The text importer inspects the text descriptors found within the braces and creates the movie whose first frame is shown in Figure 10.11. Unfortunately, we don't have space to investigate text descriptors in more detail here.
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 code for Edit menu item adjusting 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 the book, 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 NewMovieIrack and NewIrackMedia to create a new track and media, call Begi nMedi aEdi ts 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 InsertMediaIntoIrack 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: What is the format of the data in the media samples? And, 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 n u m b e r of characters in that string. And the appropriate sample description is a text description structure, defined by the TextOescri pti on data type:
294
Chapter 10 Word Is Out
struct TextDescription { long long long short short l ong l ong RGBColor Rect ScrpSTElement char
};
descSize; dataFormat; resvdl; resvd2; dataReflndex; di spl ayFl ags; textJusti fi cati on; bgColor; defaul tTextBox; defaul tStyl e; defaul tFontName[1] ;
The fii"st 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 just 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 dfScroll In and dfScrol 1Out.) The defaul tTextBox 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 t e x t J u s t i f i c a t i o n 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 teCenter teFlushRight teFlushLeft
=0, =
1~
= -.1D =
-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:
Text Tracks 295
RGBColor
myBGColor = {Oxffff, Oxffff, O x f f f f } ;
(**mySampI eDesc), bgCoI or = myBGCoI or;
The last two fields of the TextDescription structure indicate the desired text style and font. We'll ignore these fields here. Listing 10.5 shows a segment of the QTText_AddTextTrack function, w h i c h 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 10.5 Adding a text media sample. TextDescri pt i onHandl e Handle UInt16 RGBColor
mySampleDesc = NULL; mySample = NULL; myLength; myBGColor = {Oxffff, Oxffff, O x f f f f } ;
mySampleDesc = (TextDescriptionHandle)NewHandleClear(sizeof(TextDescription)) ; i f (mySampleDesc == NULL) goto bai I ; (**mySampleDesc) .descSize = sizeof(TextDescription) ; (**mySampl eDesc), dataFormat = TextMedi aType; (**mySampleDesc) .displayFlags = dfCl ipToTextBox; (**mySampleDesc) .textJusti fication = teCenter; (**mySampl eDesc), defau I t TextBox = myBounds; (**mySampleDesc).bgColor = myBGColor; myLength = EndianU16_NtoB(mySampleText[O]) ; / / create the text media sample: a 16-bit length word followed by the text myErr = PtrToHand(&myLength, &mySample, sizeof(myLength)) ; i f (myErr == noErr) { myErr = PtrAndHand((Ptr) (&mySampleText[1]), mySample, mySampleText[O]) ; i f (myErr == noErr) AddMediaSample(myMedia, mySample, O, GetHandleSize(mySample), myTextSampl eDurati on, (SampleDescri pti onHandl e)mySampleDesc, 1, O, NULL); Di sposeHandl e (mySample) ; Di sposeHandl e ( (Handl e) mySampleDesc) ;
296
Chapter 10 Word Is Out
The Movie Toolbox also provides the TextMediaAddTextSample function, which allows us to simplify this process significantly. Indeed, all of the w o r k done in Listing 10.5 can be accomplished with this single line of code:
myErr = TextMediaAddTextSample( myHandler, (Ptr) (&mySampleText [i] ), mySampleText [0], O, O, O, NULL, NULL, teCenter, &myBounds, dfCl i pToTextBox, O, O, O, NULL, myTextSampleDurat i on, NULL); The function TextMediaAddTextSample takes 17 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 GetMedi aHandl er 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 TextMediaAddTextSampl e, 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.
Text Tracks 297
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, O, myHeight) ; SetTrackMatrix(myTextTrack, &myMatrix) ; All we've done here is translate the matrix d o w n w a r d 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 o r Disabling a T e x t T r a c k 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 w h e t h e r 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 fa] se 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, w h e n looking for chapter tracks. Similarly, the QuickTime plug-in searches all text tracks, even disabled ones, w h e n looking for an HREF track. Indeed, chapter tracks and HREF tracks are usually disabled.
Creating a T e x t T r a c k Listing 10.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
298
Chapter 10 Word Is Out
theFrames is an array of integers; each element of that array indicates h o w m a n y video frames a text sample is to span. The sum of all the values in theFrames should equal the total n u m b e r of frames in the video track. Finally, the i sChapterTrack parameter indicates w h e t h e r the new text track is to be a chapter track; if i sChapterTrack 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 10.6 Adding a text track. Track QTText_AddTextTrack (Movie theMovie, char *theStrings[], short theFrames[], short theNumFrames, OSType theType, Boolean isChapterTrack) Track Track Media MediaHandler Ti meScaI e Matri xRecord Fixed Fixed OSErr
myTypeTrack = NULL; myTextTrack = NULL; myMedia = NULL; myHandler = NULL; myTi meScaI e; myMatri x; myWidth; myHeight; myErr = noErr;
/ / get the ( f i r s t ) track of the specified type; / / this track determines the width of the new text track / / and ( i f i sChapterTrack is true) is the target of the new chapter track myTypeTrack = GetMovieIndTrackType(theMovie, 1, theType, movieTrackMediaType); i f (myTypeTrack == NULL) goto bai I ; / / 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) ; i f (myTextTrack == NULL) goto bai I ; myMedia = NewTrackMedia(myTextTrack, TextMediaType, myTimeScale, NULL, 0); i f (myMedia == NULL) goto bai I ;
Text Tracks 2 9 9
myHandler = GetMediaHandler(myMedia) ; i f (myHandler == NULL) goto bai I ; / / figure out the text track geometry GetTrackMatrix(myTextTrack, &myMatrix) ; TranslateMatrix(&myMatrix, O, myHeight) ; SetTrackMatrix(myTextTrack, &myMatrix) ; SetTrackEnabled(myTextTrack, true) ; / / edit the track media myErr = Begi nMediaEdi ts (myMedia) ; i f (myErr == noErr) { Rect myBounds; short mylndex; TimeVal ue myTypeSampl eDurati on; TimeRecord myTimeRec; myBounds, top = O; myBounds, l e f t = O; myBounds.right = Fix2Long(myWidth) ; myBounds.bottom = Fix2Long(myHeight) ; / / determine the duration of a sample in the track of the specified type myTypeSampleDuration = QTUti I s GetFrameDuration(myTypeTrack) ; for (mylndex = O; mylndex < theNumFrames; mylndex++) { TimeVaI ue myTextSampI eDurat i on; Str255 mySampleText; myTextSampleDuration = myTypeSampleDuration * theFrames[mylndex] ; / / set the time scale of the media to that of the movie myTimeRec.value.lo = myTextSampleDuration; myTi meRec,value, hi = 0; myTimeRec.scale = GetMovieTimeScale(theMovie) ; ConvertTimeScale(&myTimeRec, GetMediaTimeScale(myMedia)) ; myTextSampleDuration = myTimeRec.value.lo; QTText_CopyCStringToPascal (theStrings[mylndex], mySampleText) ; / / Listing 10.5 omitted at this point, for space reasons
300
Chapter l O Word ls Out
}
}
/ / write out the new data to the media myErr = TextMediaAddTextSample( myHandler, (Ptr) (&mySampleText [1] ), mySampleText [0], O, O, O, NULL, NULL, teCenter, &myBounds, dfCl i pToTextBox, O, O, O, NULL, myTextSampleDurati on, NULL);
myErr = EndMediaEdi ts (myMedia) ; i f (myErr l= noErr) goto bai I ; / / insert the text media into the text track myErr = InsertMedialntoTrack(myTextTrack, O, O, GetMediaDuration(myMedia), fixed1); i f (myErr l= noErr) goto bai I ; / / set the text-handling procedure TextMedi aSetTextProc (myHandler, gTextProcUPP, (long) QTFrame_GetWindowObject FromFrontWi ndow() ) ; / / set the new text track as a chapter track for the track of the specified type i f (i sChapterTrack) AddTrackReference(myTypeTrack, myTextTrack, kTrackReferenceChapterList, NULL); bail: return (myTextTrack) ;
}
For the m o m e n t , you can ignore the calls to TextMediaSetTextProc and AddTrackReference. I'll explain t h e m a little later.
Text Tracks 301
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 = TextMedi aFi ndNextText (myHandl er, (Ptr) (&theText [ I ] ) , theText [ 0 ] , myFlags, myTimeVal ue, &myFoundTime, &myFoundDur a t i on, &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 findTextCaseSensitive findTextReverseSearch findTextWrapAround
= = = =
1 1 1 1
<< << << <<
O, 1, 2, 3,
findTextUseOffset
= 1 << 4
These constants are pretty m u c h 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 TextMedi aFindNextText 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:
302
Chapter 10 Word Is Out
myFl ags = findTextUseOffset; i f (!gSearchForward) myFlags [= findTextReverseSearch; i f (gSearchWrap) myFlags [= findTextWrapAround; i f (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. ~ i c a l l y , 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 $0"
myNewTime,value, hi = O; myNewTime.value.lo = myFoundTime; myNewTime.scale = GetMovieTimeScale(myMovie) ; myNewTime,base = NULL; MCDoAction(myMC, mcActionGoToTime, &myNewTime);
And we can use the TextMediaHiliteTextSample function to highlight the selected text: myColor.red = myColor.green = myColor.blue = Ox8000; TextMediaHiliteTextSample(myHandler, myFoundTime, gOffset, gOffset + theText[O], &myColor);
/ / gray
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:
Text Searching 303
myErr = MovieSearchText (myMovie, (Ptr) (&theText I l l ),
theText [0], myF1ags, NULL, ~myTimeVal ue, &gOffset) ;
W h e n 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 just listed, as well as any of these additional flags that are specific to the MovieSearchText function: enum { search Text DontGoToFoundTi me searchTextDontHi I i teFoundText searchTextOneTrackOnl y searchText EnabI edTrac ksOnI y
};
= 1L << 1 6 , = 1L << 1 7 , = 1L << 1 8 , = 1L << 19
Including either of the first two flags allows us to override the default go-toand-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 searchTextOneTrackOnl y 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 NULLon input. Listing 10.7 contains the definition of our function QTText_FindText, which we use to search for text. As you can see, it uses either the TextMedi aFindNextText function or the MovieSearchText function, depending on the value of the compiler flag USE_MOVIESEARCHTEXT.
Listing 10.7 Finding some text. void QTText_FindText (WindowObject theWindowObject, Str255 theText)
{
ApplicationDataHdl Movie MediaHandler
31t}4 Chapter 10 Word Is Out
myAppData = NULL; myMovie = NULL; myHandler = NULL;
MovieController 1ong TimeVal ue OSErr
myMC = NULL; myFl ags = OL; myTimeVal ue; myErr = noErr;
myAppData = (ApplicationDataHdl) QTFrame_GetAppDataFromWindowObject (theWi ndowObject ) ; i f (myAppData == NULL) return; myMC = (**theWindowObject).fController; myMovie = (**theWindowObject).fMovie; myHandler = (**myAppData).fTextHandler; / / set the search features myFlags = findTextUseOffset; i f (!gSearchForward) myFlags I = findTextReverseSearch; i f (gSearchWrap) myFlags I = findTextWrapAround; i f (gSearchWithCase) myFlags I = findTextCaseSensitive; myTimeValue = GetMovieTime(myMovie, NULL); #if USE MOVIESEARCHTEXT myFlags I = searchTextEnabledTracksOnly; myErr = MovieSearchText(myMovie, (Ptr) (&theText[1]), theText[O], myFlags, NULL, &myTimeValue, &gOffset); i f (myErr != noErr) QTFrame_Beep(); / / i f the desired string wasn't found, beep #else i f (myHandler i= NULL) { TimeVal ue myFoundTime, myFoundDuration; TimeRecord myNewTime; RGBColor myColor; myColor.red = myColor.green = myColor.blue = Ox8000;// gray / / search for the specified text myErr = TextMediaFi ndNextText (myHandler, (Ptr)(&theText[1]), theText[Ol, myFlags, myTimeValue, &myFoundTime, &myFoundDuration, &gOffset); i f (myFoundTime ! = -1) { / / convert the TimeValue to a TimeRecord myNewTime.value.hi = O;
Text Searching 305
myNewTime.value.lo = myFoundTime; myNewTime.scale = GetMovieTimeScale(myMovie) ; myNewTime,base = NULL; / / go to the found text MCDoAction(myMC, mcActionGoToTime, &myNewTime); / / highlight the text TextMedi aHi I i teTextSampl e (myHandler, myFoundTime, gOffset, gOffset + theText[O], &myColor); } else { QTFrame_Beep() ;
}
}
/ / i f the desired string wasn't found, beep
#endif / / update the current offset, i f we're searching forward i f (gSearchForward && (myErr == noErr)) gOffset += theText[O] ;
Of course, your code w o n ' 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 with another sample that occupies the same location in the track (that is, one that has the same starting point and duration as the original sample). W h e n the user selects the Edit Current Text m e n u item, we'll display the dialog box shown in Figure 10.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: How can we get the text of the current text sample (to put into the dialog box w h e n it's first displayed)? And, how can we determine the starting time and duration of the current text sample? Let's take these tasks in order.
306
Chapter 10 Word Is Out
Figure 10.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 applicationdefined text callback procedure that we've previously installed by calling TextMediaSetTextProc {see Listing 10.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 where we can find it w h e n we are about to display the dialog box shown in Figure 10.12. Listing 10.8 shows our application's text callback procedure.
Listing 10.8 Getting the current text. PASCAL_RTN OSErr QTText_TextProc (Handle theText, Movie theMovie, short *theDisplayFlag, long theRefCon)
{
#pragma unused(theMovie, theRefCon) char *myTextPtr = NULL; short myTextSi ze; 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 i t s e l f myTextSize = EndianU16 BtoN(* (short *) (*theText)) ; myTextPtr - (char *) (*theText + sizeof(short)); / / copy the text into our global variable for (mylndex = 1; mylndex <= myTextSize; mylndex++, myTextPtr++) gSampleText [mylndex] = *myTextPtr; gSampleText [0] = myTextSize;
Text Editing
307
//
ask f o r the d e f a u l t t e x t d i s p l a y
*theDi spl ayFl ag = txtProcDefaul tDi splay; r e t u r n (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 5tr255. Finally, we return the value txtProcOefaultDisplay in the parameter theOisp] ayFl ag; 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 k n o w 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 GetTrackNextlnterestingTime function inside the OTText EditText function. GetTrackNextlnterestingTime 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 << O,
nextTimeMediaEdit
= I << 1,
nextTimeTrackEdit
= 1 << 2,
nextTimeSyncSampl e
= 1 << 3,
nextTimeStep
= 1 << 4,
nextTimeEdgeOK
= I << 14,
nextTimelgnoreActiveSegment
= 1 << 15
For present purposes, we'll use the two flags nextTimeMediaSample and nextTimeEdgeOK, which tell GetTrackNextlnterestingTime to search in the next sample in the track's media but to consider samples that begin or end at the search starting time.
308
Chapter 10 Wordls Out
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: GetTrac kNext Interest i ngTime( myTrack, nextTimeEdgeOK [ nextTimeMediaSample, myMovieTime, -fixed1, &myI nteres t i ngTime, 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 backward 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 NULLbecause 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 = mylnteresti ngTime; GetTrac kNext I nteres t i ngTi me( myTrack, nextTimeEdgeOK I nextTimeMediaSample, myMovieTime, fixed1, NULL, &myDurati on) ;
In this case, we want only the duration of the sample returned to us, so we pass NULLin 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, mylnterestingTime, myDuration) ;
Text Editing 309
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 TextMedi aAddTextSample 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 c u r r e n t media sample in the media time scale, like this: myMovieTime = GetMovieTime(myMovie, NULL); myMediaCurrentTime = TrackTimeToMediaTime(myMovieTime, myTrack) ; MediaTi meToSampleNum( myMedi a, myMedi aCurrent Ti me, &myMedi aSampleIndex, &myMediaSampleStartTime, &myMediaSampleDurati on) ;
So we've got all the information we need to call TextMediaAddTextSample and InsertMedialntoTrack and thereby complete the text sample editing operation. For the complete definition of QTText_Edi tText, 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{ kTrackReferenceChapterLi st kTrackReferenceTimeCode kTrackReferenceModi fi er
};
= FOURCHARCODE('chap'), = FOURCHARCODE('tmcd'), = FOURCHAR 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
310
Chapter 10 Word Is Out
the next chapter.) A track reference of type kTrackReferenceModifier is used to create a modifier track; modifier tracks are useful w h e n you w a n t 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 chapters, 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 Qui ckTimeVRFormat.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 p a r a m e t e r 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 p a r a m e t e r to NULL. All the chapter titles must be contained in a single text track; we specify the starting time for chapters w h e n we add the text to the text track by calling TextMedi aAddTextSample. Note that we need to create the chapter association only between the text track and one other target track, not b e t w e e n 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 w a n t 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 nonchapter 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 p a r a m e t e r 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 10.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 p a r a m e t e r i sChapterTrack determines w h e t h e r the first text track in the movie becomes a chapter track or is demoted from that lofty rank.
Chapter Tracks 311
Listing 10.9 Setting and unsetting chapter tracks. OSErr QTText_SetTextTrackAsChapterTrack (WindowObject theWindowObject, OSType theType, Boolean isChapterTrack)
{
ApplicationDataHdl Movie MovieController Track Track OSErr
myAppData= NULL; myMovie = NULL; myMC = NULL; myTypeTrack = NULL; myTextTrack = NULL; myErr = paramErr;
/ / get the movie, controller, and related stuff myAppData = (AppI i cat i onDataHd I ) QTFrame_GetAppData FromWindowObject (theWi ndowObject) ; i f (myAppData == NULL) return (myErr) ; myMovie = (**theWindowObject).fMovie; myMC = (**theWindowObject).fController; myTextTrack = (**myAppData).fTextTrack; i f ((myMovie I= NULL) && (myMC I= NULL)) { myTypeTrack = GetMovieIndTrackType(myMovie, 1, theType, movieTrackMediaType I movieTrackEnabledOnly) ; i f ((myTypeTrack l= NULL) && (myTextTrack I= NULL)) { / / add or delete a track reference, as determined by the desired final state i f (i sChapterTrack) myErr = AddTrackReference(myTypeTrack, myTextTrack, kTrackReferenceChapterList, NULL) ; else myErr = DeleteTrackReference(myTypeTrack, kTrackReferenceChapterList, 1) ; / / t e l l the movie controller we've changed aspects of the movie MCMovieChanged(myMC, myMovie) ; / / stamp the movie as d i r t y (**theWindowObject).flsDirty = true;
return (myErr) ;
312
Chapter 10 Word Is Out
Note that after we call AddTrackReference or DeleteTrackReference, we
need to call MCMovieChanged to inform the movie controller that w e ' v e 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 n u m b e r of other chapter track utilities. Listing 10.10 defines the one we use for determining w h e t h e r a track is a chapter track; we call this function w h e n we need to determine w h e t h e r to place a check m a r k next to the Chapter Track m e n u item. Listing 10.10 Determining whether a track is a chapter track. Boolean QTText_IsChapterTrack (Track theTrack)
{
Movie Track long long long long
myMovie = NULL; myTrack = NULL; myTrackCount = OL; myTrRefCount = OL; myTracklndex; myTrReflndex;
myMovie = GetTrackMovie(theTrack) ; i f (myMovie == NULL) return(false); myTrackCount = GetMovieTrackCount (myMovie) ; for (myTracklndex = 1; myTracklndex <= myTrackCount; myTracklndex++) { myTrack = GetMovielndTrack(myMovie, myTracklndex) ; i f ((myTrack ! = NULL) && (myTrack ! = theTrack)) { / / i t e r a t e through all track references of type kTrackReferenceChapterList myTrRefCount = GetTrackReferenceCount (myTrack, kTrackReferenceChapterLi st) ; for (myTrReflndex = 1; myTrReflndex <= myTrRefCount; myTrRefIndex++) { Track myRefTrack= NULL; myRefTrack = GetTrackReference(myTrack, i f (myRefTrack == theTrack) return(true) ;
kTrackReferenceChapterList, myTrReflndex) ;
return(false);
Chapter Tracks 3 1 3
Hypertext Reference Tracks A hypertext reference track, or HREF track, is a text track in w h i c h some or all of the samples contain hypertext links, in the form of URLs. {Actually, there's no r e q u i r e m e n t that any of the samples in an HREF track contain a hypertext link, but in that case 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 n a m e 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 10.11 defines the function OTText_SetTextTrackAsHREFTrack that we can use to set and unset a text track as an HREF track.
Listing 10.11 Setting and unsetting HREFtracks. OSErr QTText_SetTextTrackAsHREFTrack (Track theTrack, Boolean isHREFTrack)
(
OSErr
myErr = noErr;
myErr = QTUtils SetTrackName(theTrack, i sHREFTrack ? kHREFTrackName 9kNonHREFTrackName); return (myErr) ;
A track's
name
is
stored
as
part
of
the
track's
user
data,
so
QTUti I s_SetTrackName (defined in QTUti I i t i es. c) calls SetUserDataltem to set the name. In QTText_SetTextTrackAsHREFTrack, we use these constants for the
track names: #define kHREFTrackName #define kNonHREFTrackName
"HREFTrack" "Text Track"
Ideally, each track should have a unique n a m e (though this is not required). So instead of hard-coding the name for the non-HREF track, we can generate a track n a m e dynamically, looking at the n a m e s that are already assigned to tracks in the movie. QTOtilities.c defines a function, OTUtils_MakeXrackNameByXype, that we can call to accomplish this. Reworking QTText_SetTextTrackAsHREFTrack to use QTUtils_MakeTrackNameByType is left as an exercise for the reader.
31/1 Chapter 10 Word Is Out
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 10.12, returns a Boolean value that indicates whether a given text track is an HREF track. Listing 10.12 Determining whether a track is an HREF track. Boolean QTText_IsHREFTrack (Track theTrack)
{
Boolean char
isHREFTrack = f a l s e ; *myTrackName = NULL;
myTrackName = QTUti I s GetTrackName(theTrack) ; i f (myTrackName ! = NULL) isHREFTrack = (strcmp(myTrackName, kHREFTrackName) == O) ; free (myTrackName) ; r e t u r n ( i sHREFTrack) ;
S o m e L o o s e 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 that are not erased by the movie controller. For example, if we've added some text in parallel, we might see something like Figure 10.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. 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 10.13 shows our updated version of QTApp_Draw. Some Loose Ends 315
Figure 10.13 A movie window after adding in parallel. Listing 10.13 Redrawing a movie window. void QTApp_Draw (WindowReference theWindow) GrafPtr GrafPtr WindowPtr Rect
mySavedPort = NULL; myWindowPort = NULL; myWindow = NULL; myRect;
Get Port (&mySavedPort ) ; myWindowPort = QTFrame_GetPortFromWindowReference(theWi ndow) ; myWindow = QTFrame GetWindowFromWindowReference (theWi ndow) ; m
i f (myWindowPort == NULL) return; MacSetPort (myWindowPort) ; # i f TARGET API MAC CARBON GetPortBounds (myWindowPort, &myRect); #else myRect = myWindowPort->portRect; #endif
316
Chapter 10 Word Is Out
Begi nUpdate(myWindow) ; i f (QTFrame_IsDocWindow(theWindow)) EraseRect (&myRect) ; 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 10.14 shows the lines we'll add to the end of QTFrame_HandleEdi tMenuItem.
Listing
10.14 Invalidating a movie window.
/ / i f the size of the movie might have changed, invalidate the entire movie box i f ((theMenultem == IDM_EDITUNDO) II (theMenultem == IDM_EDITCUT) II (theMenultem == IDM_EDITPASTE) II (theMenultem == IDM EDITCLEAR)) { Rect myRect; #if TARGET OS WIN32 RECT myWinRect; #endi f m
MCGetControl I erBoundsRect(myMC, &myRect); #if TARGET OS MAC Inval WindowRect(QTFrame_GetWindowFromWindowReference(theWi ndow), &myRect); #endif #if TARGET OS WIN32 QTFrame_ConvertMacToWinRect (&myRect, &myWinRect) ; InvalidateRect(theWindow, &myWinRect, false) ; #endif
}
Some Loose Ends 3 1 7
With these changes made, we should see no glitches like those in Figure 10.13.
Conclusion We've covered a fair a m o u n t of ground in this chapter. We've seen h o w to create text tracks, chapter tracks, and HREF tracks; w e ' v e also learned h o w to search a text track and edit the data in a text track. Along the way, w e ' v e seen how to upgrade our Edit m e n u item adjusting and our movie w i n d o w redrawing, so that even our applications that are not directly concerned w i t h 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 w a y to accomplish some particular task. We've seen, for instance, that we can call either AddMediaSampl e or TextMedi aAddTextSampl e to add media samples to a text track. And, we can call either TextMediaFindNextText or MovieSearchText to search for text within a text track. W h i c h 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 w h e n we consider the a m o u n t of source code we need to write and the kinds of details (like endian issues) to which we need to attend. In the future, we'll generally opt for the simpler, cleaner way of solving our p r o g r a m m i n g tasks (and leave the dinosaur bones for the archeologists).
318
Chapter 10 Word Is Out
Timecode
Introduction It's often useful--especially when editing some video or audio media--to be able to assign a precise time stamp to a place in a stream of media data. In 1967, the Society of Motion Picture and Television Engineers (SMPTE) introduced a method for doing this, called timecoding. Timecoding is a way of putting time stamps [called timecodes] on recorded media such as film, video, sound, and the like. QuickTime, of course, has its own methods of specifying times within a movie, using time bases, time values, and time Scales. [See Chapter 2, "Control," for a preliminary discussion of these concepts.) But QuickTime also supports storing and displaying SMPTE timecodes. Figure 11.1 shows a frame of a QuickTime movie with a timecode value displayed below the video track. Timecode is displayed using an eight-digit, 24-hour clock that looks like this: hh:mm:ss:ff. The digits represent {from left to right) hours, minutes, seconds, and frames. As you can see, the frame displayed in Figure 11.1 is exactly 14 seconds from the beginning of the movie {assuming that the movie began at timecode 00:00:00:00). The actual timecode values of a specific QuickTime movie can be synthesized by the computer or captured along with the source material. In this chapter, we'll take a look at using timecodes in QuickTime movies. Timecodes are stored in timecode tracks, which are created and managed using the timecode media handler. The timecode media handler is extremely easy to work with; it provides a grand total of 11 functions {of which we'll use only 7 here) and requires us to work with only a handful of data structures. In fact, there is only one mildly difficult concept we'll need to understand: the notion of drop-frame timecode. We'll begin by investigating the standard timecode formats, and then we'll move on to seeing how to create and manage timecode tracks in QuickTime movies.
319
Figure 11.1 A movie frame with timecode displayed.
Figure 11.2 The Test menu of QTTimeCode.
Our sample application for this chapter is called QTTimeCode; its Test menu contains the timecode-specific commands. Figure 11.2 shows the Test menu for movies that do not contain a timecode track. Figure 11.3 shows the Test menu for movies that do contain a timecode track.
320
Chapter 11 Timecode
Figure 11.3 The Test menu of QTFimeCode (second version).
Timecode Standards As we've seen, timecode is expressed using an eight-digit clock, w h e r e the rightmost two digits represent a frame number. There are four s t a n d a r d frame rates used commercially a r o u n d the world, so there are four standard timecode formats: 9 Most film produced in the United States uses a standard of 24 frames per second (fps). 9 Most film produced in Europe and Australia uses a standard of 25 fps, established by the E u r o p e a n Broadcasting U n i o n (EBU); 25 fps is also used for PAL or SECAM video and television. 9 The SMPTE standard for American black-and-white television uses 30 fps. This frame rate is also c o m m o n l y used for audio and CD mastering. 9 American color television and systems based on it {such as NTSC video} use a standard of 29.97 fps. This rate is the result of certain technical decisions made w h e n converting from black-and-white to color television. The oddball here is the SMPTE standard for American color television and NTSC video, which uses a frame rate of 29.97 fps. This is close enough to 30 that people typically consider American color TV to have a frame rate of 30 fps. But 29.97 isn't exactly 30, and the difference can add up. For instance, over a period of one hour, a 30-fps movie will display 108,000 frames, whereas a 29.97-fps movie will display 107,892 frames. This means that, if we use a 30-fps timecode to display the time of a 29.97-fps movie, the movie would be 108 frames (or about 3.6 seconds} longer than a movie that has exactly 30 fps. That is to say, by the time a 29.97-fps movie gets to 108,000 frames, 1 hour and 3.6 seconds has elapsed. For most purposes, an extra 3.6 seconds per hour is pretty insignificant. But on commercial TV, time is money. For example, ABC charged about $2.2 million for a 30-second advertisement during the 2001 Super Bowl. In that
Timecode Standards
321
Figure 11.4 A movie frame with drop-frame timecode.
case, 3.6 seconds is worth about $264,000. So it's sometimes important to get timecode values exactly right. To ensure that 30-fps timecodes displayed by a 29.97-fps movie exactly match the time on a wall clock, SMPTE defined drop-frame tirnecoding, according to which two frame numbers are dropped at the start of every minute, except for every minute that is evenly divisible by 10. (For example, the timecode value 11:31:59:29 is immediately followed by 11:32:00:02.) So, over the course of one hour, 108 frame numbers will be dropped, which is exactly what is needed to bring the 30-fps timecode display into synchronization with a wall clock. In spite of the name ("drop-frame timecode"), no video frames are dropped. Rather, frame numbers are dropped from the timecode display so that the timecode stays in synch with a wall clock. When drop-frame timecode is being used, it's conventional to use a comma (',') or a semicolon (';') to separate the seconds digits from the frames digits. Figure 11.4 shows a QuickTime movie that uses drop-frame timecode. Keep in mind that in a timecode, the rightmost two digits represent a frame number, not {say) hundredths of a second. This is worth remarking, if only because there are occasions where QuickTime does use the rightmost digits to represent fractions of a second. For instance, in the previous chapter, we saw that the text movie importer recognizes a number of text descriptors,
322
Chapter 11 Timecode
one of which is a time stamp indicating the time at which the imported text sample is to begin. In that case, the rightmost digits represent a n u m b e r of time scale units.
TJmecode in QuJckTime QuickTime version 2.0 introduced the timecode media handler, which we can use to create timecode tracks in movies, hide and show those tracks, get and set information about timecode tracks, and so forth. The timecode media handler supports both drop-frame and n o n - d r o p - f r a m e timecodes, as well as a simple counter (shown in Figure 11.5). A timecode track specifies timing information for one or more other tracks, which are typically video or sound tracks. To associate the timecode track with a target track, the target track contains a track reference to the timecode track. All of the QuickTime functions for working with timecodes take an instance of the timecode media handler as a parameter. Our sample application QTTimeCode creates a single timecode track in a movie, so we can call GetMovielndTrackType and GetMediaHandler to find a movie's timecode media handler, as shown in Listing 11.1.
Figure 11.5 A movie frame with a counter.
Timecode in QuickTime
323
Listing 11.1 Finding a movie's timecode media handler instance. MediaHandler QTTC_GetTimeCodeMediaHandler (Movie theMovie)
{
Track Media Medi aHandl er
myTrack -- NULL; myMedia = NULL; myHandler = NULL;
/ / get the ( f i r s t ) timecode track in the specified movie myTrack = GetMovielndTrackType(theMovie, 1, TimeCodeMediaType, movieTrackMediaType) ; i f (myTrack i= NULL) ( / / get the timecode track's media and media handler myMedia = GetTrackMedia(myTrack) ; i f (myMedia l= NULL) myHandler = GetMediaHandl er(myMedi a) ;
}
return (myHandler) ;
When the user selects the Add Timecode Track c o m m a n d in the Test m e n u of our QTTimeCode application, QTTimeCode displays the dialog box shown in Figure 11.6 to elicit some settings from the user. As you can see, QTTimeCode allows the user to specify the name of the media source (which might be the name of the tape from which the video was digitized). The user can also indicate w h e t h e r the timecode should be displayed at all, and (if so) whether the timecode should be displayed below the video. QTTimeCode allows the user to specify the font in which the timecode is displayed and the starting time of the timecodes. It's worth emphasizing that a timecode track in a QuickTime movie does not affect the actual timing of the movie playback. QuickTime's own timing facilities always determine the rate at which frames in a movie are displayed. If, for example, we play a movie back at twice its normal rate, the timecodes would also zip by at twice the speed of a wall clock.
T i m e c o d e Tracks We add a timecode track to a movie in exactly the same way we've added other kinds of tracks. The main difference, of course, is that the media type of the new track will be TimeCodeMediaType. To know how to add a timecode track to an existing movie, we need to know only two things: the format of the data in a timecode media sample and the structure of a sample description for a timecode media sample. Let's tackle these issues in reverse order.
3124 Chapter11Timecode
Figure 11.6 Q1-FimeCode'sTimecode Options dialog box.
We'll also consider how to position the timecode track and configure some of the text options.
Creating a Timecode Sample Description A fimecodesample description is defined by the TimeCodeDescription data type, like this:
struct TimeCodeDescription { descSize; long dataFormat; long resvdl; long resvd2; short dataReflndex; short flags; long timeCodeDef; TimeCodeDef srcRef[1] ; long
};
Timecode Tracks 325
The first five fields are common to all sample descriptions, and we need to fill in only the descSize and dataFormat fields: TimeCodeDescri pti onHandl e long
myDesc = NULL; mySi ze;
mySize = sizeof(TimeCodeDescription) ; myDesc = (TimeCodeDescriptionHandle)NewHandleClear(mySize); i f (myDesc == NULL) goto bai I ; (**myDesc).descSize = mySize; (**myDesc) .dataFormat = TimeCodeMediaType;
The flags field is reserved and should be set to O. The timeCodeDef field is a timecode definition structure that contains information about the desired format of the timecode. A timecode definition structure is defined by the TimeCodeDef structure: struct TimeCodeDef { long Ti meScaI e TimeVaI ue UInt8 UInt8
};
flags; fT i meScaI e; frameDu rat i on; numFrames; padding;
typedef struct TimeCodeDef TimeCodeDef;
The flags field of the TimeCodeDef structure contains a set of flags that specify information about the format of the timecode. Currently, these flags are defined: enum { tcDropFrame tc24HourMax tcNegTimesOK tcCounter
};
= 1 <<0,
=1<<1,
= 1 <<2,
=1<<3
The tcDropFrame flag, of course, indicates whether the timecode is dropframe or non-drop-frame timecode. The tc24HourMax flag indicates whether the hours digits return to 0 at 24 hours or keep advancing. The tcNegTimes0K flag indicates whether negative timecode values are allowed. Finally, the tcCounter flag indicates whether to display the simple counter instead of a SMPTE timecode.
326
Chapter 11 Timecode
The numFrames field of the TimeCodeDef structure indicates the n u m b e r of frames in each second of media data in the target track. If the simple counter is being displayed, this field indicates the n u m b e r of frames that make up each increment of the counter. The frameDuration field indicates the duration, in time units defined by the fTimeScale field, of each frame in the target media. QTTimeCode keeps track of most of these settings by defining a set of global variables (which makes it easy to get information into and out of the Timecode Options dialog box). Using some of these globals, we set up the timecode definition structure as shown in Listing 11.2.
Listing 11.2 Setting up a timecode definition structure. / / set the timecode format information flags i f (gUseTimeCode) { myFl ags = OL; i f (gDropFrameVal) myFlags I = tcDropFrame; i f (glsNeg) myFlags I = tcNegTimesOK; i f (g24Hour) myFlags I = tc24HourMax; } else { myFl ags = tcCounter;
}
myTCDef, fl ags = myFlags; myTCDef.fTimeScale = gTimeScale; myTCDef.frameDuration = gFrameDur; myTCDef.numFrames = gNumFrames;
The last field in a timecode sample description is the srcRef field, which is defined as an array containing a single long integer. This field contains information about the source of the timecode values. This is typically the name of the tape or other source of the media data from which the timecode values were originally captured. The interesting thing here is that the data in this field must be formatted in the same way as a piece of movie or track user data. As a result, we can create the source information using QuickTime's user data functions like NewUserData and AddUserDataText. But the source information is not stored in the same location as movie or track user data; instead, it's stored in the timecode sample description itself. The timecode media handler provides a function, TCSetSourceRef, that we can use to put the source information into the timecode sample description. Listing 11.3 shows how we set the source information for a timecode track. Notice that the user data item is of type TCSourceRefNameType.
Timecode Tracks 3 2 7
Listing 11.3 Setting the source information. UserData
myUserData;
myErr = NewUserData(&myUserData) ; i f (myErr --= noErr) { Handle myNameHandle = NULL; myErr = PtrToHand(&gSrcName[1], &myNameHandle, gSrcName[O]); i f (myErr == noErr) { myErr = AddUserDataText(myUserData, myNameHandle, TCSourceRefNameType, 1, I angEngl i sh) ; i f (myErr == noErr) TCSetSourceRef(myHandler, myDesc, myUserData) ;
},
i f (myNameHandle ! = NULL) Di sposeHandl e (myNameHandle) ; Di sposeUserData (myUserData) ;
W h y isn't the source information stored as part of the track's user data (in an atom of type 'udta')? Part of the reason is that there can be more than one timecode sample in a timecode track, and each sample may be associated with timecode information from a different source. So it makes sense to attach the source information to the timecode sample description. In any event, we really don't need to worry about where the source information is stored, since we shall always use the functions TCSetSourceRef and TCGetSourceRef to set and get that information. {For what it's worth, QTTimeCode will create only one timecode sample, which spans the length of the entire target track.} TCSetSourceRef updates the descSize field of the timecode sample description, so we don't need to do this ourselves.
Creating a Timecode Media Sample Now we need to see how to create a media sample for our timecode track. The sample description contains information about the format and source of the timecode. The start time and duration of the media sample indicate the start time and duration of the timecode display. All that remains is to specify the timecode value of the first frame of the target track segment. That is to say, w h e n the timecode media handler encounters a timecode sample, it needs to know what the timecode value is at that moment. This is precisely what's contained in a timecode media sample.
328
Chapter 11 Timecode
A timecode media sample contains a long integer that specifies the timecode value of the first frame in the target track that is to have a timecode value associated with it. The timecode value is stored as a frame number, which is converted automatically to a timecode value by the timecode media handler. The timecode media handler provides functions for converting timecode values into frame values and vice versa. To see how this works, let's suppose that we want the first frame of a movie to have the timecode 08:23:11;00. Suppose further that the four parts of this timecode value are contained in the global variables gHours, gMinutes, gSeconds, and gFrames. Ultimately we want to call the function TCTimeCodeToFrameNumber to determine the frame n u m b e r that we'll put into the media sample. We specify the
timecode value to TCTimeCodeToFrameNumber by passing it a pointer to a timecode record, defined like this: union TimeCodeRecord { Ti meCodeTime t; Ti meCodeCounter c;
};
typedef union TimeCodeRecord TimeCodeRecord;
This is a union that contains either a timecode time record or a tirnecode counter record: struct TimeCodeTime { UInt8 UInt8 UInt8 UInt8
};
hours; minutes; seconds; frames;
typedef struct TimeCodeTime TimeCodeTime; struct TimeCodeCounter { long counter;
};
typedef struct TimeCodeCounter TimeCodeCounter;
Since we've got a timecode value that we want to convert into a frame number, we'll use the t variant, like this: myTCRec.t.hours = (Ulnt8)gHours; myTCRec.t.mi nutes = (Ulnt8)gMinutes; myTCRec,t.seconds = (UInt8)gSeconds; myTCRec.t.frames = (UInt8)gFrames; i f (glsNeg) myTCRec,t .mi nutes I = tctNegFl ag;
Timecode Tracks 329
Notice that the mi nutes field of the timecode time record should have the bit tctNegF1 ag (namely, Ox80) set if the timecode value is to be interpreted as a negative value. We're finally ready to create the timecode media sample data. Since AddMediaSample wants a handle to the media data, we need to allocate a reloo catable block large enough to hold a long integer and then call TCTimeCodeToFrameNumber to convert the timecode stored in myTCRec into a frame number. Then we need to ensure that the frame number is stored in big~ endian format, like this: myFrameHandle = (long **)NewHandle(sizeof(long)) ; i f (myFrameHandle == NULL) goto bai I ; myErr = TCTimeCodeToFrameNumber(myHandler, &(**myDesc).timeCodeDef, &myTCRec, *myFrameHandle) ; **myFrameHandle = EndianS32 NtoB(**myFrameHandle) ; D
Then we proceed as usual, calling Begi nMedi aEdi ts, AddMediaSampl e, EndMediaEdits, and InsertMediaIntoTrack.
Setting the Timecode Track Geometry For simplicity, the QTTimeCode application uses the first video track in a movie as the target track (that is, the track that contains a track reference to the timecode track). We'll call GetTrackOimensions on the target track to get the desired width of the timecode track. For the initial height of the timecode track, we'll use a predefined constant. Ideally, however, the height of the timecode track should depend on the height of the timecode display, which depends of course on the font and point size of the characters in the timecode display. We can retrieve information about the timecode display by calling TCGetDispl ayOpti ons, which returns information in a text options structure, defined by the TCTextOptions data type: struct TCTextOptions { short short short short RGBColor RGBColor
};
txFont; txFace; txSize; pad; foreCol or; bac kCol or;
typedef struct TCTextOptions TCTextOptions;
330
Chapter 11 Timecode
The txFont field contains the font n u m b e r of the timecode display font. W h e n setting up a timecode track, we want to do two things with this font. First, we want to reset the txFont field to the font n u m b e r of the font that the user selected in our application's Timecode Options dialog box (see Figure 11.6 again). Then we want to measure that font to determine the maxim u m height for our timecode track. When the user closes the Timecode Options dialog box by clicking the OK button, QTTimeCode gets the index and name of the currently selected font by executing these two lines of code: gFontlndex = GetControl Value(myControl ) ; GetMenultemText(myMenu, gFontlndex, gFontName);
Then, w h e n creating a timecode track, QTTimeCode uses the font n a m e stored in the gFontName global variable to find the appropriate font number: TCGetDi sp I ayOpti ons (myHandler, &myTextOpti ons) ; GetFNum(gFontName, &myTextOptions.txFont) ; TCSetDi sp I ayOpti ons (myHandler, &myTextOpti ons) ;
Now we need to determine the m a x i m u m height of the timecode display. We can do this by getting a representative string containing a timecode and then measuring that string in the current font, style, and size" TCTimeCodeToString(myHandler, &myTCDef, &myTCRec, myString); Text Font (myTextOptions. txFont) ; TextFace (myTextOpti ons. txFace) ; TextSi ze (myTextOpti ons. txSi ze) ; GetFont Info (&myFontInfo) ; myTCHeight = FixRatio(myFontlnfo.ascent + myFontlnfo.descent + 2, 1);
You'll notice that we determine the height of the timecode track by adding the distance from the font baseline to the ascent line and the distance from the baseline to the descent line (plus a couple of pixels of extra space). In some fonts, the numerals have no descenders, so this calculation will tend to make the timecode look out of center vertically within the timecode track. Devising a better algorithm is left as an exercise for the reader. Now we've determined the proper height and width for the timecode track. At this point, we can reset the track dimensions: SetTrackDimensions(myTrack, myWidth, myTCHeight);
Timecode Tracks 331
Then, if the user has indicated that the timecode track should appear below the video track, we can reset the track matrix, like this: GetTrackMatrix(myTrack, &myMatrix) ; i f (gDi spl ayBel owVideo) TranslateMatrix(&myMatrix, O, myHeight) ; SetTrackMatrix(myTrack, &myMatrix) ;
The timecode track is now positioned at the desired location with the appropriate width and height. We can configure the track to be displayed by calling TCSetTimeCodeFlags with the tcdfShowTimeCode flag. TCSetTimeCodeFlags(myHandler, gDisplayTimeCode ? tcdfShowTimeCode : O, tcdfShowTimeCode) ;
A timecode track must be enabled in order for it to be displayed, so we should call SetTrackEnabled before calling TCSetTimeCodeFlags.
Creating a T r a c k Reference The final thing we need to do w h e n creating a timecode track is to create a track reference from the target track to the timecode track, like this: myErr = AddTrackReference(myTypeTrack, myTrack, kTrackReferenceTimeCode, NULL);
Listing 11.4 shows our complete function for creating a timecode track in a movie. Listing 11.4 Adding a timecode track to a QuickTime movie. OSErr QTTC_AddTimeCodeToMovie (Movie theMovie, OSType theType) Track Track Media MediaHandler TimeCodeDef Ti meCodeRecord Str63 TimeVal ue Matri xRecord
332
Chapter 11 Timecode
myTypeTrack = NULL; myTrack = NULL; myMedia = NULL; myHandler = NULL; myTCDef; myTCRec; myString; myDurati on; myMatri x;
Fixed Fixed Fixed long TCTextOpt ions Font Info TimeCodeDescriptionHandle l ong OSErr
myWidth; myHeight; myTCHeight; myFl ags = OL; myTextOptions; myFont Info; myDesc = NULL; **myFrameHandl e; myErr = noErr;
myTypeTrack = GetMovielndTrackType(theMovie, i f (myTypeTrack == NULL) { myErr : trackNotlnMovie; goto bai I ;
1, theType, movieTrackMediaType) ;
}
/ / get the dimensions of the target track GetTrackDimensions(myTypeTrack, &myWidth, &myHeight) ; / / create the timecode track and media myTrack = NewMovieTrack(theMovie, myWidth, kTimeCodeTrackSize, kNoVolume); i f (myTrack == NULL) goto bai I ; myMedia = NewTrackMedia(myTrack, TimeCodeMediaType, GetMovieTimeScale(theMovie), NULL, 0)1 i f (myMedia == NULL) goto bai I ; myHandler : GetMediaHandler(myMedia) ; i f (myHandler == NULL) goto bai I ; / / f i l l in a timecode d e f i n i t i o n structure; / / set the timecode format information flags i f (gUseTimeCode) { myFl ags = OL; i f (gDropFrameVal) myFlags I = tcDropFrame; i f (glsNeg) myFlags I = tcNegTimesOK; i f (g24Hour) myFlags I = tc24HourMax;
Timecode Tracks
333
} else { myFlags = tcCounter;
}
myTCDef,flags = myFlags; myTCDef.fTimeScale = gTimeScale; myTCDef.frameDuration = gFrameDur; myTCDef.numFrames = gNumFrames; / / f i l l in a timecode record i f (gUseTimeCode) { myTCRec.t.hours = (UInt8)gHours; myTCRec.t.minutes = (UInt8)gMinutes; myTCRec.t.seconds = (UInt8)gSeconds; myTCRec.t.frames = (UInt8)gFrames; i f (glsNeg) myTCRec,t .minutes I = tctNegFl ag; } else { myTCRec,c. counter = gCounterVal ;
}
/ / figure out the timecode track geometry / / get display options to calculate box height TCGetDisplayOptions(myHandler, &myTextOptions) ; GetFNum(gFontName, &myTextOptions.txFont) ; TCSetDi spl ayOpti ons (myHandler, &myTextOptions) ; / / use the starting time to figure out the dimensions of track TCTimeCodeToString(myHandler, &myTCDef, &myTCRec, myString); TextFont (myTextOpti ons. txFont) ; TextFace (myTextOpti ons. txFace) ; TextSi ze (myTextOptions. txS i ze) ; Get Font Info (&myFontInfo) ; / / calculate track width and height based on text myTCHeight = FixRatio(myFontlnfo.ascent + myFontlnfo.descent + 2, 1); SetTrackDimensions(myTrack, myWidth, myTCHeight) ; GetTrackMatrix(myTrack, &myMatrix) ; i f (gDi spl ayBel owVideo) TranslateMatrix(&myMatrix, O, myHeight) ; SetTrackMatrix(myTrack, &myMatrix) ; SetTrackEnabled(myTrack, gDisplayTimeCode ? true : false); TCSetTimeCodeFlags(myHandler, gDisplayTimeCode ? tcdfShowTimeCode : O, tcdfShowTimeCode);
334
Chapter 11 Timecode
/ / edit the track media myErr = BeginMediaEdits(myMedia) ; i f (myErr == noErr) { long mySize; UserData myUserData; / / create and configure a new timecode description handle mySize = sizeof(TimeCodeDescription) ; myDesc = (TimeCodeDescriptionHandle) NewHandleClear(mySize) ; i f (myDesc == NULL) goto bai I ; (**myDesc) .descSi ze = mySize; (**myDesc) .dataFormat = TimeCodeMediaType; (**myDesc). timeCodeDef = myTCDef; / / set the source i d e n t i f i c a t i o n information / / the source i d e n t i f i c a t i o n information for a timecode track is stored / / in a user data item of type TCSourceRefNameType myErr = NewUserData(&myUserData) ; i f (myErr == noErr) { Handle myNameHandl e = NULL; myErr = PtrToHand(&gSrcName[1], &myNameHandle, gSrcName[O]) ; i f (myErr --= noErr) { myErr = AddUserDataText(myUserData, myNameHandle, TCSourceRefNameType, 1, langEnglish) ; i f (myErr -= noErr) TCSetSourceRef(myHandl er, myDesc, myUserData) ;
}
if
(myNameHandle ! = NULL) Di sposeHandl e (myNameHandle) ;
Di sposeUserData (myUserData) ; / / add a sample to the timecode track myFrameHandle = (long **)NewHandle(sizeof(long)) ; i f (myFrameHandle == NULL) goto bai I ; myErr = TCTimeCodeToFrameNumber(myHandl er, &(**myDesc). t imeCodeDef, &myTCRec, *myFrameHandle) ; / / the data in the timecode track must be big-endian **myFrameHandle = EndianS32 NtoB(**myFrameHandle) ; D
Timecode Tracks 335
myDuration = GetMovieDuration(theMovie) ; / / since we created the track with the same time scale as the movie, / / we don't need to convert the duration myErr = AddMediaSample(myMedia, (Handl e) myFrameHandle, O, GetHandI eSi ze ( (HandI e) myFr ameHandI e), myDuration, (SampleDescriptionHandle)myDesc, 1, O, 0); i f (myErr ! = noErr) goto bai] ; myErr = EndMediaEdi ts (myMedia) ; i f (myErr != noErr) goto bail ; myErr = InsertMedialntoTrack(myTrack, O, O, myDuration, fixed1); i f (myErr ! = noErr) goto bai I ; / / create a track reference from the target track to the timecode track myErr = AddTrackReference(myTypeTrack, myTrack, kTrackReferenceTimeCode, NULL); bail: i f (myDesc ! = NULL) Di sposeHandl e ( (Handl e)myDesc) ; i f (myFrameHandle ! = NULL) Di sposeHandl e( (Handl e) myFrameHandle) ; return (myErr) ;
Timecode Track Operations Now that we know how to create timecode tracks, let's take a look at a few c o m m o n ways of operating on timecode tracks. We m a y want to get information about them, show and hide them, and delete them.
336
Chapter 11 Timecode
Getting Information about a Timecode Track The timecode media handler provides several functions that we can use to get and set information about a timecode track in a movie. For example, if we w a n t to get the current timecode value for a movie, we can call the TCGetCurrentTimeCode function. Listing 11.5 defines the OTTC_ShowCurrentTimeCode function, w h i c h Q T T i m e C o d e uses to get the current timecode value and display it in a dialog box to the user.
Listing 11.5 Displaying the current timecode value. void QTTC_ShowCurrentTimeCode (Movie theMovie)
{
MediaHandler HandlerError TimeCodeDef Ti meCodeRecord
myHandler = NULL; myErr = noErr; myTCDef; myTCRec;
myHandler = QTTC_GetTimeCodeMediaHandler(theMovi e) ; i f (myHandler ! = NULL) { / / get the timecode for the current movie time myErr = TCGetCurrentTimeCode(myHandler, NULL, &myTCDef, &myTCRec, NULL); i f (myErr == noErr) { Str255 myString; myErr = TCTimeCodeToString(myHandler, &myTCDef, &myTCRec, myString); i f (myErr == noErr) QTTC_ShowStringToUser(myStri ng) ;
As you can see, QTTC_ShowCurrentTimeCode first retrieves the instance of the timecode media handler and then calls TCGetCurrentTimeCode to get the current timecode value. Then it passes the timecode definition structure and timecode record filled in by TCGetCurrentTimeCode to the TCTimeCodeToString function, to convert the timecode value into a string. Finally, QTTC ShowCurrentTimeCode calls the application function QTTC_ShowStringToUser to show the string to the user. Similarly, we can display a timecode track's source information to the user by calling the QTTC_ShowTimeCodeSource function defined in Listing 11.6.
Timecode Track Operations 337
Listing 11.6 Displaying the timecode source information. void QTTC_ShowTimeCodeSource (Movie theMovie)
{
Medi aHandl er HandlerError UserData
myHandl er = NULL; myErr = noErr; myUserData;
myHandl er = QTTC_GetTimeCodeMediaHandl er (theMovi e) ; i f (myHandler ! = NULL) { / / get the timecode source for the current movie time myErr = TCGetCurrentTimeCode(myHandler, NULL, NULL, NULL, &myUserData); i f (myErr == noErr) { Str255 myString = " [No source name!]"; Handle myNameHandle = NewHandleClear(O) ; GetUserDataText (myUserData, myNameHandle, TCSourceRefNameType, 1, I angEngl i sh) ; i f (GetHandleSize(myNameHandle) > O) { BlockMove(*myNameHandle, &myString[1], GetHandleSize(myNameHandle)) ; myString[O] = GetHandleSize(myNameHandle) ;
}
if
(myNameHandle ! = NULL) Di sposeHandl e (myNameHandle) ;
QTTC_ShowStri ngToUser(myStri ng) ; Di sposeUserData (myUserData) ;
} }
One thing to notice here is that the TCGetCurrentTimeCode function returns, t h r o u g h its last parameter, a user data list that contains the source information of a timecode track. (We d i d n ' t need the source information in Listing 11.5, so there we passed NULL for the last parameter.) Once w e ' v e successfully retrieved that information, we can use the Movie Toolbox user data function GetUserDataText to find the source information and then display it to the user.
Showing and Hiding a Timecode Track As w e ' v e seen in earlier chapters, w e can show or hide a specific track in a movie by enabling or disabling that track using the Movie Toolbox function
338
Chapter11Timecode
SetTrackEnabled. For example, we can toggle the visible state of a track with
this line of code: SetTrackEnabled(myTrack, !GetTrackEnabled(myTrack)) ;
With a timecode track, we also need to keep the media handler informed of the visible state of the track by setting the timecode flags appropriately, like this: TCGetTimeCodeFl ags (myHandler, &myFlags) ; myFl ags ^= tcdfShowTimeCode; TCSetTimeCodeFl ags (myHandler, myFlags, tcdfShowTimeCode) ;
Here, we simply retrieve the current timecode flags, toggle the tcdfShowTimeCode bit, and then send the new set of flags back to the timecode media handler. The third parameter to the TCSetTimeCodeFlags function is a mask that indicates which bits in the myF1ags parameter to consider. As you can see, we want the media handler to change only the tcdfShowTimeCode bit itself. Listing 11.7 shows the complete definition of the 0TTC_ToggleTimeCodeDisplay function. Listing 11.7 Toggling the visible state of a timecode track. void QTTC_ToggleTimeCodeDisplay (MovieController theMC)
{
Movie Track MediaHandler l ong
myMovie = MCGetMovie(theMC); myTrack = NULL; myHandler= NULL; myFl ags = OL;
/ / get the ( f i r s t ) timecode track in the specified movie myTrack = GetMovielndTrackType(myMovie, 1, TimeCodeMediaType, movieTrackMediaType); i f (myTrack ! = NULL) { / / get the timecode track's media handler myHandler = QTTC_GetTimeCodeMediaHandler(myMovi e) ; i f (myHandler ! = NULL) { / / toggle the show-timecode flag TCGetTimeCodeFl ags (myHandler, &myFlags) ; myFlags ^= tcdfShowTimeCode; TCSetTimeCodeFl ags (myHandler, myFlags, tcdfShowTimeCode) ; / / toggle the track enabled state SetTrackEnabled(myTrack, !GetTrackEnabled(myTrack)) ;
Timecode Track Operations
339
}
}
/ / now tell the movie controller the movie has changed, / / so that the movie rectangle gets updated correctly MCMovieChanged(theMC, myMovie) ;
Notice that we call MCMovieChanged to p r o m p t the movie controller to reset its state based on the current properties of the associated movie. This causes the movie controller to recalculate the controller rectangle and r e d r a w the movie.
Deleting a Timecode Track Finally, we might want to remove a timecode track from a movie. Our sample application QTTimeCode supports the Remove Timecode Tracks m e n u item; w h e n the user selects this item, QTTimeCode calls the QTTC_DeleteTimeCodeTracks function, which was originally defined as in Listing 11.8.
Listing 11.8 Deleting all timecode tracks in a movie. void QTTC_DeleteTimeCodeTracks (Movie theMovie)
{
Track
myTrack = NULL;
myTrack = GetMovielndTrackType(theMovie, 1, TimeCodeMediaType, movieTrackMediaType); while (myTrack l= NULL) { Di sposeMovieTrack (myTrack) ; myTrack = GetMovielndTrackType(theMovie, 1, TimeCodeMediaType, movieTrackMediaType) ;
}
This is straightforward: just keep looking for tracks of media type TimeCodeMedi aType and dispose of them. Unfortunately, this also leaves the movie file in a somewhat untidy state, as it does not remove the track reference that we added w h e n we called AddTrackReference w h e n creating a timecode track. This "dangling" track reference w o n ' t cause any real h a r m until we decide to add another timecode track to the movie, in which case it will end up confusing QuickTime mercilessly. So, before we dispose of a timecode track, we need to delete any references that other tracks have to it. We'll do that by adding a call to the utility function QTUti 1s_Del eteA11ReferencesToTrack just before the call to Di sposeMovi eTrack. QTUti ] s_Del eteA11ReferencesToTrack is defined in Listing 11.9. As
340
Chapter11Timecode
you can see, it loops through all the tracks in the movie and looks at all the track references attached to each of those tracks; if any of those track references points to the timecode track that we want to delete, we'll delete that track reference. Listing 11.9 Deleting all timecode tracks in a movie. OSErr QTUtils DeleteAlIReferencesToTrack (Track theTrack) Movie Track long long long l ong OSErr
myMovie = NULL; myTrack = NULL; myTrackCount = OL; myTrRefCount = OL; myTrackIndex; myTrRef Index; myErr = noErr;
myMovie = GetTrackMovie(theTrack) ; i f (myMovie == NULL) return (paramErr) ; / / iterate through all the tracks in the movie myTrackCount = GetMovieTrackCount(myMovie) ; for (myTracklndex = 1; myTracklndex <= myTrackCount; myTracklndex++) { myTrack = GetMovielndTrack(myMovie, myTracklndex) ; i f ((myTrack I= NULL) && (myTrack ! = theTrack)) { OSType myType = OL; / / iterate through all track reference types contained in the current track myType = GetNextTrackReferenceType(myTrack, myType); while (myType l= OL) { / / iterate through all track references of the current type; / / note that we count down to 1, since DeleteTrackReference will cause / / any higher-indexed track references to be renumbered myTrRefCount = GetTrackReferenceCount(myTrack, myType); for (myTrReflndex = myTrRefCount; myTrReflndex >= 1; myTrReflndex--) { Track myRefTrack= NULL; myRefTrack = GetTrackReference(myTrack, myType, myTrReflndex); i f (myRefTrack == theTrack) myErr = DeleteTrackReference(myTrack, myType, myTrReflndex);
Timecode Track Operations
341
}
}
myType = GetNextTrackReferenceType(myTrack, myType) ;
return (myErr) ;
Conclusion As we've seen, it's quite easy to create and manipulate timecode tracks. These tracks take up very little space in a movie file, and they can be extremely useful in coordinating QuickTime movies with their external source material. While timecode display is principally useful to video or audio editors, the counter display can serve a variety of purposes.
342
Chapter 11 Timecode
2001: A Space Odyssey
Introduction As you know, QuickTime was originally developed to run on Macintosh computers. In the mid-1990s, Apple released a version of QuickTime {called Quick T~'me for Windows) that provided support for playing QuickTime movies on Windows computers. While it was a significant step forward, this version had some severe limitations. Most important, it provided a playback engine only; there was no API for creating QuickTime movies on the Windows platform. Also, many of the APIs for playing movies back differed from their Macintosh counterparts. For instance, on the Mac, NewMovieControl let is declared essentially like this: MovieController NewMovieController (Movie theMovie, const Rect *movieRect, long someFlags);
But under QuickTime for Windows, it had this declaration: MovieControl ler NewMovieController (Movie theMovie, const LPRECTIprcMovieRect, long someFlags, HWNDhWndParent);
You'll notice that the Windows version took an additional parameter (hWndParent) and that the type of the second parameter was a pointer to the standard Windows rectangle type (RECT), not the Macintosh rectangle type {Rect). I myself never wrote any code using the original QuickTime for Windows APIs, but I can imagine that it was something of a headache for experienced Macintosh programmers. Just flipping casually through the documentation is enough to convince me that it was probably impossible to develop any very interesting QuickTime code that would compile and link on both platforms. My guess is that anyone developing a QuickTime-savvy application for both
343
Mac and Windows platforms probably had to maintain two separate source code bases. QuickTime 3.0 changed all that. It provided, for the first time, a set of APIs that were virtually identical--in both parameter lists and feature comp l e t e n e s s - o n Macintosh and Windows platforms. In other words, it is now possible to write Mac and Windows applications that use the same source code, at least for the QuickTime-specific portions of the application. You know that I've placed a great deal of emphasis throughout this book on developing code that is completely cross-platform. In this chapter, we'll take a more in-depth look at some of the platform-specific issues that we've considered only in passing up to now {such as working with multibyte data and resourcesl. The magic provided by the Windows version of QuickTime 3.0 was accomplished principally by a library called the QuickTime Media Layer (or, more briefly, QTMLI. The QuickTime Media Layer provides an implementation of a number of the parts of the Macintosh Operating System [including the Memory Manager and the File Manager) and the Macintosh User Interface Toolbox (including the Dialog Manager, the Control Manager, and the Menu Managerl. QuickTime, of course, also runs on Mac OS X, which is a UNIX-based operating system. In this case, the implementation of the Macintosh Operating System and Toolbox managers is provided by a library called Carbon, which we encountered earlier. It should come as no surprise to learn that Carbon originally grew out of the existing work on QTML. We're going to approach this chapter in this way: let's suppose that we've got a QuickTime-savvy application that runs under the "classic" Macintosh operating systems (that is, Mac OS 8.x and 9.x). We want to see what we need to do to get it to run also under the various flavors of Windows (namely, Windows 95/98/ME, Windows NT/2000, and Windows XPI and under Mac OS X.
E n d i a n Issues Let's begin this odyssey at the lowest {and sometimes most vexingl level, by considering the memory storage of multibyte data. Motorola processors in the 680x0 family expect multibyte data to be stored with the most significant byte lowest in memory. This is known as big-endian byte ordering {because the "big" end of the data value comes first in memory I. Intel and other processors used for Windows machines expect multibyte data to be stored with the least significant byte lowest in memory; this is known as little-endian byte ordering. Figure 12.1 shows the value 0x12345678 stored in both Motorola 680x0 and Intel formats. The PowerPC family of processors supports both big- and little-endian byte orderings, but uses big-endian w h e n running Macintosh software.
3~1~ Chapter 12 2001: A Space Odyssey
II ox,2 t ox3, i ox,, i ox,8 II Big-endian byte ordering
Figure 12.1
II ox,8 i ox,, I ox,, i ox,2 II Little-endian byte ordering
Big- and little-endian data storage.
This difference is i m p o r t a n t only in one instance, namely, w h e n a s t r e a m of data is transferred b e t w e e n the c o m p u t e r ' s m e m o r y and some external container (such as a file). For instance, if we open a file that has its data stored in big-endian format and read a segment of it into memory, we should not operate upon that data w i t h o u t first m a k i n g sure that we convert the data into the proper endian format for the processor w e ' r e r u n n i n g on. This conversion is k n o w n as byte swapping. Let's consider a few real-life cases w h e r e byte swapping comes into play.
W o r k i n g w i t h M o v i e User Data A movie's user data is stored inside of atoms in a QuickTime file. W h e n we call the function GetMovieUserData to get a movie's user data, the Movie Toolbox reads that atom into m e m o r y and returns a handle to that data to us. We can get a specific piece of movie user data by calling GetUserDataItem and passing it the type of data we want. For instance, we can retrieve the stored w i n d o w location of a movie by reading the user data item of type 'WLOC'.Listing 12.1 defines the function OTUtils_GetWindowPositionFromFile, w h i c h returns the stored location of the top-left corner of the movie window. Listing 12.1 Getting the stored position of a movie window. OSErr QTUtils GetWindowPositionFromFile (Movie theMovie, Point *thePoint)
{
UserData Point OSErr
myUserData = NULL; myPoint = {kDefaultWindowX, kDefaultWindowY}; myErr = paramErr;
/ / make sure we've got a movie i f (theMovie == NULL) goto bai I ; / / get the movie's user data l i s t myUserData : GetMovieUserData(theMovie) ; i f (myUserData != NULL) { myErr = GetUserDataltem(myUserData, &myPoint, sizeof(Point), FOUR CHAR CODE('WLOC'), O) ; i f (myErr == noErr) { myPoint.v = EndianS16 BtoN(myPoint.v); w
m
Endian Issues 3 4 5
myPoint.h = EndianS16 BtoN(myPoint.h);
bail: i f (thePoint l= NULL) *thePoint = myPoint; return (myErr) ;
With only rare exceptions, movie user data is stored in big-endian format. So once we've read in the data stored in the 'WLOC' user data item, we need to convert that data from big-endian format into native-endian format (that is, the native format of the processor we're running on). As you can see, QTUtils GetWindowPositionFromFile uses the macro EndianS16 BtoN to convert the signed 16-bit data from big- to native-endian format. This macro is defined in the file Endi an.h, like this: #if TARGETRT BIG ENDIAN #define EndianS16 BtoN(value) #else #define EndianS16 BtoN(value) #endif
(value) EndianS16 BtoL(value) B
In other words, if we are running on a big-endian processor, then EndianS16_BtoN does nothing. But on a little-endian processor, EndianS16_BtoN is replaced by the macro EndianS16_BtoL, which in turn is replaced by the macro Endi an16_Swap: #define EndianS16_BtoL(value) ((SInt16)Endian16_Swap(value))
Finally, Endian16_Swap is defined like this: #define Endian16_Swap(value) \ (((((UIntl6)value)<<8) & OxFFO0) I ((((UIntl6)value)>>8) & OxOOFF))
If you w o r k through this, you'll see that Endianl6_Swap simply swaps the high-order byte with the low-order byte, w h i c h is just w h a t we expected. The macros for handling 32- and 64-bit values are of course more complicated, but do pretty much the same thing.
346
Chapter 12 2001"A Space Odyssey
The trick is figuring out when to swap bytes. Here are the guidelines I like to follow: 9 Byte swapping is required only for multibyte data (that is, data that's 16 bits or larger). For smaller chunks of data, we don't need to w o r r y about it. So, for example, if we retrieve a user data item whose data is a Boolean value, we don't need to swap any bytes. 9 Byte swapping is not required for C or Pascal strings, even though these are typically multibyte. Strings are i n t e r p r e t e d as arrays of single-byte characters, so they are identical on big- and little-endian architectures. 9 Byte swapping is required only w h e n data is to be transferred b e t w e e n RAM and some external container (such as reading data from or writing data to a file). For in-memory calculations or moving data b e t w e e n buffers, we don't need to w o r r y about the endianness of the data. 9 Note that the Endian16_Swap macro evaluates its argument several times. It w o u l d be a bad idea, therefore, to use i n c r e m e n t (++) or d e c r e m e n t (--) operators in these macros. (To be safe, don't use these operators in any macros, since the definitions of the macros may change. Believe me, this has bitten me more than once.) In general, any data stored in a QuickTime file is in big-endian format. This includes movie user data, data in atoms and atom containers, stored sample descriptions, and media data itself. In some rare cases, media data may be stored in little-endian format, but the appropriate media handler should insulate us from having to worry about that.
Reading and Writing Resource Data Data stored in Macintosh resource files is always big-endian. There is, however, an important difference between the data returned to us by the File Manager and the data returned to us by the Resource Manager. To wit: the Resource Manager always gives us native-endian data. In other words, if we use Resource Manager functions to open [for instance) a 'pnot' resource, we can read the fields of that resource {which, as we've seen in earlier chapters, has the structure of a preview resource record, of type PreviewResourceRecord} without having to flip any multibyte data. The same holds true of writing data to a resource: we give the Resource Manager a handle to a block of native-endian data, and it will ensure that the data actually written to the resource file is in big-endian format. This automatic byte swapping is accomplished by a piece of code called a resource flipper. A resource flipper takes a handle of data and flips the multibyte data fields in that handle, in place. Of course, a resource flipper needs to have intimate knowledge of the structure of the resource we're handing
Endian Issues 347
it. QTML includes resource flippers for these resource types (current as of version 4.1.2)" 'ALRT' 'CNTL' 'DLOG' 'icl8'
'BNDL' 'cpix' 'FREF' 'ICN#'
'MBAR'
'MENA'
'qfmt' 'stgp' 'thar' 'vers'
'qmtr' 'STR ' 'thga' 'WIND'
'cdci ' ' crsr 'fttl 'ICON 'MENU 'qtet 'STR# 'thnd 'wxim
cicn' CURS' gnht' ics4' nini' snd ' styl' thng'
'clut' 'DITL' 'icl4' 'ics8' 'pnot' 'stg#' 'TEXT' 'thnr'
But w h a t if we w a n t to w o r k with a resource that isn't one of these types (say, a custom resource used only by our application)? Here we have at least two choices. First, we can just make a local copy of the fields of the data in the resource handle r e t u r n e d by the Resource M a n a g e r w h e n e v e r we need to access them and then flip those fields ourselves. For instance, let's suppose that we have a custom resource of type 'CHAP' that consists of a signed 16-bit count followed by that n u m b e r of signed 32-bit integers (perhaps to record the start times of the chapters of our movie). We might use this data structure to help us read and write the resource data: struct ChapterResourceRecord { SInt16 fNumChapters; SInt32 fChapters[1];
};
typedef struct ChapterResourceRecord ChapterResourceRecord; typedef ChapterResourceRecord *ChapPtr, **ChapHdl;
We can open a resource of type 'CHAP' and get the n u m b e r of chapters in the resource like this: ChapHdl
myChapters = NULL;
myChapters = (ChapHdl)GetResource(FOUR CHAR CODE('CHAP'), kMyChapResourceID) ; i f (myChapters ! = NULL) myNumChaps = EndianS16_BtoN((**myChapters). fNumChapters) ;
Another way to handle this is to tell the Resource Manager how to flip the data in our custom resource. Then it can do all the necessary w o r k for us w h e n e v e r we read or write a resource of that type, leaving us to w o r k always with native-endian data. We do this by installing a resource flipper for our custom resource type by calling Regi sterResourceEndianFi I ter, w h i c h has this declaration:
348
Chapter 12 2001A Space Odyssey
OSErr RegisterResourceEndi anFi I ter (ResType theType, ResourceEndianFilterPtr theFilterProc) ; As you can see, we pass RegisterResourceEndianFilter the type of our custom r e s o u r c e a n d a p o i n t e r to o u r r e s o u r c e flipper. T h e d a t a t y p e ResourceEndianFi 1t e r P t r is d e c l a r e d like this: typedef OSErr (*ResourceEndianFi I terPtr) (Handle theResource, Boolean currentlyNativeEndian);
For o u r c u s t o m 'CHAP' resource, w e could w r i t e o u r r e s o u r c e flipper as s h o w n in Listing 12.2. Listing 12.2. Flipping the data in a custom resource type. OSErr QTXP ChapterResourceFlipper (Handle theHandle, Boolean currentlyNativeEndian)
{
Ptr short short
myDataPtr = NULL; myNumChaps= O; myCount;
II
i f ((theHandle == NULL) return (nil Handl eErr) ;
(*theHandle == NULL))
/ / see how many longs are in this resource data myDataPtr = *theHandl e; myNumChaps = *(short *)myDataPtr; / / i f we're flipping from big- to native-endian, we need to f l i p myNumChaps / / or else our loop l i m i t w i l l be t e r r i b l y wrong i f (!currentlyNativeEndian) myNumChaps = EndianS16 BtoN(myNumChaps); m
/ / f i r s t , f l i p the fNumChapters f i e l d . *(short *)myDataPtr = Endian16_Swap(*(short *)myDataPtr); myDataPtr += sizeof(short) ; / / now f l i p the chapter data for (myCount = O; myCount < myNumChaps; myCount++) { *(long *)myDataPtr = Endian32_Swap(*(long *)myDataPtr); myDataPtr += sizeof(long) ;
}
return (noErr) ;
Endian Issues 3 4 9
The second parameter passed to our resource flipper, currentlyNativeEndi an, is a Boolean value that indicates whether the data in the handle specified by the first parameter is in native-endian format. If it's native-endian, then we can read the data in that handle and use it without first converting it. Otherwise, we need to convert any of that data that we use in our flipper. In QTXP_ChapterResourceF1 ipper, for instance, we need to flip the 16-bit count field if it's not already in native-endian format, since we use that value in the for loop. We install our custom resource flipper like this: myErr = RegisterResourceEndi anFi I ter(FOUR CHAR CODE( 'CHAP' ), QTXP ChapterResourceFl i pper) ; D
Keep in mind that resource flippers are necessary only on non-Macintosh platforms, because there is no need to byte-swap resource data on the Mac. {In other words, the resource storage format and the m e m o r y storage format are the same on Macintosh computers.} Occasionally we might need to know whether a resource flipper is installed for a particular resource type. We can determine this by calling Regi sterResourceEndianFilter to try to install a new resource flipper for that type. If a resource flipper is already installed, RegisterResourceEndianFi l ter returns the value 1; otherwise, it returns noErr.
The Quick'rime Media Layer As we learned earlier, the QuickTime Media Layer is a Windows implementation of those parts of the Macintosh Operating System and the Macintosh User Interface Toolbox that are used by QuickTime. In effect, QTML provides a sizable chunk of a Macintosh ROM cleverly packaged into a dynamic-linked library (QuickTime.qts). QuickTime APIs are riddled through and through with Macintosh data types such as handles, resources, and Pascal strings. Also, QuickTime internally calls a large n u m b e r of Mac OS and Toolbox routines, such as NewHandle, GetResource, NewContro], and the like. As we might say, you can take QuickTime out of the Mac, but you can't take the Mac out of QuickTime; so we need to drag a good bit of the Mac OS and Toolbox anywhere we want to run QuickTime. That's what QTML provides on Windows (and what Carbon provides on Mac OS X). QTML is a rock-solid product. The most important evidence for this is that it's possible to write large chunks of QuickTime code on the Mac that also compile, link, and run on Windows (assuming you've paid attention to issues like endianness of multibyte data). It just works! It's important to understand, however, that there are limitations to what you can do with QTML, and we'll consider some of those limitations in this section. The
350
Chapter 12 2001A Space Odyssey
main thing to keep in mind is that the QuickTime Media Layer is not designed as a general-purpose tool for getting any and all Mac OS or Toolbox_ functions to run on Windows. Rather, it's designed to support those parts of the Mac OS and Toolbox that are needed to run QuickTime. If you remember nothing else from this chapter, at least r e m e m b e r this: O.TML is not a porting layer.
Avoiding Namespace Collisions One of the first issues we run into w h e n porting our Macintosh-based QuickTime code to Windows is a small n u m b e r of collisions in the function namespace. That is to say, some of the Mac OS and Toolbox functions have the same names as similar Windows functions. For instance, both Mac and Windows provide functions named ShowWindow. If we try to compile our Macintosh code unchanged, we'll get a compiler error like this: Error : cannot convert 'struct GrafPort *' to 'struct HWND *' QTTimeCode.c line 426 ShowWindow(myDialog);
Luckily, the Mac header files now contain alternate names for these functions for non-Mac targets. In general, the new forms of the names simply contain the prefix "Mac," to signal that they are the Macintosh versions of these functions. For instance, the Mac version of ShowWindow has been renamed as MacShowWindow. Some other renamed functions are MacSetPort, MacInsertMenu, MacSetRect, Mac0ffsetRect, MacPtInRect, MacSetCursor, and MacCheckMenuItem. (This list is by no means exhaustive.) As a rule of thumb, if you get a compiler error where the compiler tries to convert Mac data types to Windows data types, try adding the prefix "Mac" to the function name. There were also collisions in the names of header files. The Mac OS file originally called Windows. h (which contains the public API for the Macintosh Window Manager) was renamed as MacWindows.h to avoid conflict with the Windows header file windows.h. Similarly, the file Types.h was renamed as MacTypes.h, Errors.h was renamed as MacErrors.h, Memory.h was renamed as MacMemory.h, and Help.h was renamed as MacHelp.h. (I believe that this list is exhaustive.) To my knowledge, the only data types r e n a m e d to accommodate the Windows APIs were the QuickDraw types Polygon and Region (to MacPolygon and MacRegion, respectively), and I have found only one constant in the Mac headers that causes problems w h e n compiling code under Windows. The file MoviesFormat.h contains these constants:
The QuickTime Media Layer 3511
enum { MOVIE TYPE TRACK TYPE MEDIA TYPE VIDEO TYPE SOUND TYPE };
= = = = =
D
FOUR CHAR CODE('moov'), FOUR CHAR CODE('t r a k ' ) , FOURCHAR CODE('mdia'), FOURCHAR CODE('vide'), FOUR CHAR CODE('soun') m
Unfortunately, the Windows header file winioctl .h contains an enumerated type called MEDIA_TYPE (for various types of device media). Compiling with both these header files results in an "illegal name overloading" error. When building code using Microsoft Developer Studio on Windows, I usually don't include the file winioctl .h. But when using CodeWarrior on the Mac with the precompiled Windows headers, the easiest way to avoid the collision is to edit MoviesFormat.h to comment out the definition of MEDIA_TYPE. Cheesy, but true.
Working with Flies Windows, of course, provides its own functions for opening files (for instance, CreateFile). When working with movie files on Windows, however, it's usually easier to use the routines provided by the Macintosh Standard File Package and File Manager. The main reason is that these functions use file system specifications (of type FSSpec) to refer to files, which are also used by QuickTime functions like CreateMovieFile and OpenMovieFile. For instance, we can use the following lines of code to elicit a movie file from the user and open the selected file: # i f TARGET OS WIN32 / / prompt the user for a f i l e myTypeListrO] = MovieFi leType; StandardGetFilePreview(NULL, 1, myTypeList, &myReply); i f ( !myReply. sfGood) return (userCancel edErr) ; N
/ / make an FSSpec record FSMakeFSSpec(O, OL, myReply.sfFile.name, &myFSSpec); OpenMovieFile(&myFSSpec, &myRefNum, fsRdWrPerm) ; #endif
This snippet of code also reveals that the FSSpec data type is defined differently on Mac and Windows. On Windows, the name field contains a full pathname of a file, and the parID and vRefNum fields are ignored. Also, like the Mac version, the name field should contain a Pascal string, not a C string.
352
Chapter12 2001"A SpaceOdyssey
QTML provides a useful function for converting a C-style p a t h n a m e into an FSSpec" NativePathNameToFSSpec("c:\\myMovie.mov", &myFSSpec, kFulINativePath) ;
We can also use the File Manager to open, create, and manipulate files that are not movie files. You may recall that in Chapter 5, "In and Out," we defined a function QTDX_GetExporterSettingsFromFi l e that used another function, QTDX_ReadHandleFromFile, to read the data in a file into a handle. QTDX_ReadHandl eFromFi l e is defined in Listing 12.3. Listing 12.3 Reading a file's data into a handle. Handle QTDX_ReadHandleFromFile (FSSpecPtr theFSSpecPtr)
{
Handl e short long OSErr
myHandl e = NULL; myRefNum = O; mySize = O; myErr = noErr;
/ / open the f i l e myErr = FSpOpenDF(theFSSpecPtr, fsRdWrPerm, &myRefNum); if
(myErr == noErr) myErr = SetFPos(myRefNum, fsFromStart, O) ;
/ / get the size of the f i l e data i f (myErr == noErr) myErr = GetEOF(myRefNum, &mySize) ; / / allocate a new handle i f (myErr -= noErr) myHandle = NewHandleClear(mySize) ; if
(myHandle == NULL) goto bai I ;
/ / read the data from the f i l e into the handle i f (myErr == noErr) myErr = FSRead(myRefNum, &mySize, *myHandle); bail" i f (myRefNum ! = O) FSCIose (myRefNum) ; return (myHandle) ;
The QuickTime Media Layer 3 5 3
This is all p r e t t y straightforward File M a n a g e r (and M e m o r y Manager) code, and it works as well on W i n d o w s as it does on Macintosh.
Working with Resources The Resource Manager, w h i c h reads t y p e d data from a resource file, also w o r k s very well on Windows. Perhaps the biggest a n n o y a n c e w h e n w o r k i n g with resource files on W i n d o w s m a c h i n e s is getting t h e m there in the first place. I've found that some m e a n s of transferring resource files from Macintosh c o m p u t e r s to W i n d o w s c o m p u t e r s don't w o r k v e r y well or require additional steps. For example, if I copy the Macintosh resource file MacAppl ication, rsrc onto a floppy disk and then m o u n t that floppy disk on a Windows machine, the file MacApplication.rsrc is usually 0 bytes in size. To find the actual resource data, I need to look inside a folder called Resource. frk, w h e r e the resource file n o w has the n a m e Macapp-l.rsr. Nor is copying resource files across a n e t w o r k m u c h better. If I m o u n t a W i n d o w s disk on m y Macintosh c o m p u t e r and drag the file MacApplication.rsrc to the Windows disk, two files are created on the W i n d o w s machine, MacApplicat i o n . r s r c (which again is 0 bytes in size) and MacApplication.rsrc.#Res (which is the actual resource file). Perhaps the most reliable course is simply to build the resource file on W i n d o w s from a . r file (which, being a normal text file, is easy to transfer from m a c h i n e to machine). The Q u i c k T i m e SDK includes the tool Rez for c o n v e r t i n g , r files into resource files. We can execute this line of code in the DOS Console to create a resource file: Qui ckTi meSDK\QTDevWin\Tool s\Rez -i "QuickTimeSDK\QTDevWin\RIncludes"-i QTShell.r -o QTShell.qtr
.
Here, Rez converts the resource descriptions in the file QTShe11.r into resources in the resource file QTShell . q t r (looking in the current directory and in the directory Oui ckTimeSDK\OTDevWin\RInc] udes for any i n c l u d e d , r files). The output of the Rez tool is typically a file with the extension .qtr, w h i c h is the preferred extension for resource files that are to be h a n d l e d using QTML. As we see, an application w h o s e n a m e is OTShell.exe w o u l d have a resource file w h o s e n a m e is OTShel 1 .qtr. On the Macintosh, w h e n an application is launched, the application's resource file is opened automatically and added to the resource chain. But on Windows, this is not the case. So if our W i n d o w s application uses any Macintosh-style resources, we n e e d to explicitly open the application's resource file, as illustrated in Listing 12.4.
354
Chapter 12 2001: A Space Odyssey
Listing 13.4 Opening an application's resource file. myLength = GetModuleFileName(NULL, myFileName, MAX PATH); i f (myLength l= O) { NativePathNameToFSSpec(myFileName, &gAppFSSpec, kFul INativePath) ; gAppResFile = FSpOpenResFile(&gAppFSSpec, fsRdWrPerm) ; i f (gAppResFile ! = klnvalidFileRefNum) UseResFi I e (gAppResFi le) ;
Here, we retrieve the n a m e of the file that holds the application, create an FSSpec for that file, and then pass that FSSpec to the Resource Manager function FSp0penResFile. Once this is accomplished, we can use other Resource Manager calls [like GetResource) to access the resources in that file. There is one final issue to consider. On Macintosh systems, a data fork and its associated resource fork have the same n a m e and appear in the Finder as a single file. On Windows, our application and its resources have different n a m e s and appear to the user as two different files. W h e n we ship an application to the user, it's better to combine both of these files into a single file. The QuickTime SDK includes the tool RezWack, w h i c h a p p e n d s the resource file to the executable file (and also a p p e n d s some additional data to alert QTML to the fact that the .exe file contains the resource file). We can use RezWack to combine our .exe and our . q t r files like this: QuickTimeSDK\QTDevWin\Tool s\RezWack -d QTShelI .exe -r QTShell .qtr -o TempName.exe del QTShell.exe ren TempName.exe QTShelI .exe
RezWack does not allow us to overwrite either of the two input files, so we need to save the output file u n d e r a t e m p o r a r y name, delete the original, exe file, and then r e n a m e the output file to the desired name. Even if we have Rezwack'ed our application's resource file into the executable file, we still need to explicitly open the resource file; so our applications should include the code in Listing 12.4, w h e t h e r the resource file is a separate file or is included in the executable file.
Working with Modal Dialog Boxes OK, it's p e r h a p s not all that surprising that the File Manager and the Resource Manager run pretty well on W i n d o w s u n d e r QTML. But, for me at least, it is fairly surprising that QTML provides extensive support for key
The QuickTime Media Layer 355
parts of the Macintosh User Interface Toolbox, including the Window Manager, the Dialog Manager, the Control Manager, and the Menu Manager. Certainly, if the goal is to support running QuickTime applications on Windows computers, then there needs to be support for user interaction, since QuickTime and its components can display dialog boxes to show information to or get information from the user. Let's see what issues arise w h e n porting our Macintosh dialog box code to Windows. A dialog box typically contains some small number of controls (buttons, check boxes, edit-text boxes, pop-up menus, and so forth). Controls in modal dialog boxes work very well, with minimal cross-platform dependencies. For example, in the previous chapter, we displayed the modal dialog box shown in Figure 12.2. The code for displaying this dialog box and for handling events in this box works unchanged on Windows. Figure 12.3 shows the Windows version of this dialog box. Nonetheless, I did need to make a few changes in the original Macintosh source code to achieve this cross-platform parity. First of all, I needed to adjust the way in which the plus sign (+) and the rectangle surrounding it were drawn. The code I originally inherited did something like that shown in Listing 12.5.
Figure 12.2 QTlimeCode'sTimecode Options dialog box (Macintosh).
356
Chapter 12 2001: A Space Odyssey
Figure 12.3 QTTimeCode's Timecode Options dialog box (Windows). Listing 12.5 Drawing a user item (original version). do { ModaIDialog(gModal Fi I terUPP, &myltem) ; switch (myltem) { ... / / lots of lines omitted here case k I teml sNeg: glsNeg = !glsNeg; GetDialogltem(myDialog, myltem, NULL, NULL, &myRect); MoveTo(myRect.left + 2, myRect.top + 17); MacFrameRect(&myRect) ; MaclnsetRect(&myRect, 1, I ) ; EraseRect (&myRect) ; MaclnsetRect (&myRect, -1, -1) ; TextSi ze (kTextBi gSi ze) ; i f (glsNeg) DrawString ("\p-") ; else DrawString ("\p+") ; TextSi ze (kTextRegSi ze) ; break;
The QuickTime Media Layer 357
}
default: break;
} while ((myltem ! = kStdOkltemIndex) && (myltem l= kStdCanceIItemlndex));
As you can see, this code draws into the dialog box from within the ModalDialog loop. That is to say, each time the user clicks the rectangle of the kItemIsNeg dialog item, this code erases that rectangle and redraws the "+" or "-" symbol. This happens to work just fine on Macintosh computers, but it doesn't work quite right on Windows computers. (The symbols draw OK when the user clicks in the item rectangle, but they are not redrawn if the dialog box is covered up and then uncovered.) Instead, we need to make sure we use the method recommended by the Inside Macintosh series (Apple Computer, Inc.), which is to define auser item callback procedure, as shown in Listing 12.6. Listing 12.6 Drawing a user item (revised version). PASCAL_RTN void QTTC_OptionsUserItemProcedure (DialogPtr theDialog, short theltem)
(
Handle Rect ControIHandle
myltemHandle = NULL; myRect; myControl = NULL;
i f (theltem l= kltemlsNeg) return; MacSetPort (GetDi aI ogPort (theDi aIog) ) ; GetDialogltem(theDialog, theltem, NULL, NULL, &myRect); MoveTo(myRect.left + 2, myRect.top + 17); MacFrameRect(&myRect) ; MaclnsetRect(&myRect, 1, I ) ; EraseRect (&myRect) ; MacInsetRect (&myRect, - 1, - I ) ; TextSi ze (kTextBi gSi ze) ; i f (glsNeg) DrawString ("\p-") ; else DrawStri ng ("\p+") ; TextSi ze (kTextRegSi ze) ;
358
Chapter 12 2001: A Space Odyssey
Then, before we display our dialog box, we install the callback procedure by calling GetDi al ogl tem and SetDi al ogl tem: gOpti onsUserI temProcUPP = NewUserI temProc (QTTC_Opti onsUserI temProcedure) ; GetDialogltem(myDialog, kltemlsNeg, &myKind, &myHandle, &myRect); SetDialogltem(myDialog, kltemlsNeg, myKind, (Handle)gOptionsUserltemProcUPP, &myRect);
[In fact this has always b e e n the r e c o m m e n d e d w a y to d r a w user items in dialog boxes, but you never k n o w w h e n shortcuts have been taken.) There is one final step we need to take here, w h i c h is to have the Dialog Manager call our user item callback procedure w h e n e v e r the user clicks the user item rectangle. We can do this by calling InvalRect or InvalWindowRect. Now the code for the k ItemIsNeg case should look like this: case kI teml sNeg: glsNeg = !glsNeg; GetDialogItem(myDialog, myltem, NULL, NULL, &myRect); #if TARGET API MAC CARBON Inva I WindowRect(GetDi aI ogWindow(myDiaIog), &myRect); #else Inva I Rect (&myRect) ; DrawDialog(myDialog); / / force a redraw (necessary on Windows) #endif break;
I've found that, u n d e r Windows, we also need to call DrawDial og to get the appropriate parts of the dialog box to redraw. Now look again at Figure 12.2. You'll notice that it contains a pop-up m e n u control that allows the user to select a font. W h e n I was adding the font pop-up m e n u to QTTimeCode, I diligently followed the Inside Macintosh series, according to which we can specify a resource type in the control reference constant field of a control resource and add the value popupUseResMenu (0x0004) to the control definition ID. In other words, I constructed the control resource shown in Figure 12.4. (Here, 1179602516 is 0x464F4E54, or 'FONT' in ASCII.) The Control Manager will add the names of all resources of the specified type to the pop-up m e n u w h e n it creates that menu. Unfortunately, I couldn't get this to work on Windows. Instead, I needed to programmatically add the names of the available fonts to the pop-up m e n u control, as shown in Listing 12.7.
The QuickTime Media Layer 359
Figure 12.4
ResEditdefinition of a pop-up menu control.
Listing 12.7
Adding font names to a pop-up menu control.
myControl = QTTC_GetDItemRect(myDial og, kFontPopUpMenuControl, &myRect) ; i f (myControl ! = NULL) { myMenu = MacGetMenu(kFontPopUpResI D) ; i f (myMenu ! = NULL) { # i f ACCESSORCALLS ARE FUNCTIONS / / insert the menu into the menu l i s t MaclnsertMenu(myMenu, klnsertHierarchicaIMenu) ; SetCont rol PopupMenuHandle (myControl, myMenu) ; SetControl PopupMenuID(myControl, kFontPopUpResID) ; #else PopupPri vateData myPri vateData; / / insert the menu into the menu l i s t MaclnsertMenu(myMenu, klnsertHierarchicaIMenu) ; myPrivateData.mHandle = myMenu; myPri vateData.mID = kFontPopUpResID; ( PopupPri vateData) (** ( PopupPri vateDataHandl e) (**myControl) .contrIData) = myPrivateData; #endi f / / clean out existing menu while (CountMenultems(myMenu)) DeleteMenultem(myMenu, 1) ;
360
Chapter 12
2001A Space Odyssey
/ / add in the available fonts AppendResMenu(myMenu, FOUR_CHAR_CODE(' FONT') ) ; SetControIMaximum(myControl, CountMenultems(myMenu)) ; SetControIValue(myControl, gFontlndex) ;
It would have been nice if QTML had correctly interpreted the original control resource, but the workaround is straightforward and completely cross-platform.
Working with Modeless Dialog Boxes As we've seen, modal dialog boxes (that is, dialog boxes whose event handling is accomplished using the ModalDialog function) work well on Windows, with very minor adjustments to our existing Macintosh source code. Working with modeless dialog boxes requires a bit more work, however. The main reason for this is that, because our Windows application is not driven by a Macintosh-style event loop, we cannot use the IsDialogEvent and DialogSelect functions to detect and handle events for modeless dialog boxes. Instead, we need to install a callback function to handle Windows messages generated by the main message loop. Let's suppose that we want our application to display the modeless dialog box shown in Figure 12.5 (Macintosh version) and Figure 12.6 (Windows version). This dialog box contains three radio buttons for dynamically setting the looping state of the frontmost movie window. In our Macintosh code, we can display the dialog box like this: gLoopDlg = GetNewDialog(kLoopDLOGResID, NULL, (WindowPtr)-l) ;
And then we can intercept events targeted for this dialog box in our main event loop, as shown in Listing 12.8. Listing 12.8 Handling events in a modeless dialog box (Macintosh). WaitNextEvent(everyEvent, &myEvent, kWNEDefaultSleep, NULL); i f ( I sDi aI ogEvent (&myEvent)) i f (DialogSelect(&myEvent, &myDialog, &myltemHit)) { i f (myDialog == gLoopDlg) QTXP_DoLoopDialogEvent(&myEvent, myDialog, myltemHit); continue;
}
The QuickTime Media Layer 361
Figure 12.5 A modeless dialog box (Macintosh).
Figure 12.6 A modeless dialog box (Windows).
Here, we call I sDi a l ogEvent to determine whether the event is targeted at a dialog box. Then we call DialogSelect to get the dialog pointer and the number of the item hit. Finally, we call the application function QT• to handle the event. (We won't bother to consider QT• here; it just sets the control values of the radio buttons as appropriate and changes the looping mode of the frontmost movie.) On Windows, as I've said, there is no event loop, so we can't call I sDi al ogEvent and Di al ogSe] ect. Instead, we need to install a modeless dialog box callback procedure, by calling the SetMode] essDi a] ogCa] ] backProc function: gLoopDlg = GetNewDialog(kLoopDLOGResID, NULL, (WindowPtr)-l) ; # i f TARGETOS WIN32 SetModel essDi al ogCal I backProc (gLoopDl g, (QTModelessCal I backUPP)QTXP_LoopingDi al ogCal I back) ; #endif m
QTML calls a modeless dialog box callback procedure whenever it processes a message that's targeted at an item in the modeless dialog box. QTML first translates that message into a Macintosh-style event, which it passes as a parameter to the callback procedure (along with the dialog pointer and the number of the affected dialog item). Our callback procedure, defined in Listing 12.9, is quite simple. It just calls the application function QTXP_DoLoopDialogEvent to handle the event.
]162
Chapter 12 2001A Space Odyssey
Listing 12.9 Handling events in a modeless dialog box (Windows). #if TARGETOS WIN32 void QTXP_LoopingDialogCallback (EventRecord *theEvent, DialogPtr theDialog, Dialogltemlndex theltemHit)
{
}
i f (theDialog ! = NULL) QTXP_DoLoopDial ogEvent (theEvent, theDi al og, thel temHit) ;
#endi f
Once we've added a modeless dialog box callback procedure in this way, the dialog box shown in Figure 12.6 will work as expected on Windows. {You might be wondering why we didn't just install QTXP OoLoopOialogEvent as our modeless dialog box callback procedure, since all OTXP_LoopingDia] ogCallback really does is call QTXP_OoLoopOialogEvent. The answer is that, in the future, our callback procedures will need to be more complicated; so I've opted for illustrating the more general case here, even though it isn't strictly necessary.)
Handling Strings Let's take a look at how we might need to adjust our handling of strings (that is, sequences of individual characters). Strings passed to Macintosh APIs are generally expected to be Pascal strings, which consist of a length byte followed by that number of characters. Most Macintosh C compilers recognize the '\p' escape sequence for constructing Pascal strings. For instance, Listing 12.5 contains this line: DrawString ("\p+") ;
The parameter to DrawString is a string constant, which our compiler formats as a Pascal string. In memory, this string occupies two bytes, having the value OxO12B. Similarly, the string "hello, world" occupies 13 bytes (12 characters plus a length byte). Because the length of a Pascal string is specified by a single byte, a Pascal string can contain at most OxFF (that is, 255) characters. By contrast, the C programming language supports C strings, which consist of some number of characters followed by a termination character (the byte OxO0, or '\0'). There is no length byte in a C string, so C strings can be arbitrarily long. Many Macintosh programmers--myself included--prefer to work primarily with C strings; as a result, when we want to call a function like DrawString that requires a Pascal string, we need to convert the C string to a Pascal string. In the past, I've used the function c2pstr, which converts its argument in place:
The QuickTime Media Layer 363
char myString[] = "hello, world"; DrawString (c2pstr (myStri ng) ) ;
The C string "hello, world" looks like this in memory: Ox68656C6C6F2C20776F726C6400
After the call to c2pstr, that same block of memory looks like this: OxOC68656C6C6F2C20776F726C64
As you can see, the characters in the C string have been shifted to the right, and the length byte (here, 0x0C) has been inserted before the first character; the byte formerly occupied by the termination character is now occupied by the last character in the string. To avoid this string conversion, we can use special glue functions that take C strings as parameters, rather than Pascal strings. For instance, the file Qui ckdrawText.h declares the function drawstring, which takes a C string: char myString[] = "hello, world"; drawstring (myStri ng) ;
So far, so good. But several problems arise when we try to make our string-handling code work on QTML and Carbon. First of all, these glue functions are not supported on Carbon, so we need to restrict ourselves to those functions that work with Pascal strings. Further, the functions c2pstr and p2cstr are likewise not supported on Carbon. (Instead, Carbon supports several similar functions, c2pstrcpy and p2cstrcpy.) Another problem is that not all Windows compilers support the '\p' escape sequence for creating Pascal strings. Microsoft Developer Studio Visual C/C + + versions 5 and earlier did support it, but that support was dropped in version 6. In that case, the string "\phello, world" would be misinterpreted as the C string "phello, world". To help prevent such unexpected results, QTML examines Pascal strings whose length byte is 'p' (that is, 0x70 or 112) to see if they are really misgenerated C strings; if they are, it calls c2pstr to convert them in place to bona fide Pascal strings. This is wonderful, unless our strings happen to be located in read-only memory (in which case having QTML call c2pstr on them would cause an access violation). The safest course of action is probably to avoid using the '\p' escape sequence entirely (so that Windows compilers don't ever get the chance to misinterpret it). Instead, we'll create all strings as C strings and then convert them to Pascal strings whenever we need to pass them to a function that requires Pascal strings.
364
Chapter 12 2001: A Space Odyssey
But which functions shall we use to make this conversion? Early on in my efforts to port Macintosh code to Windows, I decided it was simplest just to define my own functions for converting strings from one form into another. That way, we don't need to worry about whether any functions have been deprecated by the move to Carbon, or QTML, or whatever. Listing 12.10 defines the function QTUtils ConvertCToPascalString, which we've used many times in this book for converting C strings to Pascal strings. Listing 12.10 Converting a C string into a Pascal string. StringPtr QTUtils ConvertCToPascalString (char *theString)
{
StringPtr short
myString = malloc(min(strlen(theString) + 1, 256)); myIndex = O;
while ((theStringrmylndexl ! = ' \ 0 ' ) && (mylndex < 255)) { myString[mylndex + 1] = theString[mylndex]; mylndex++;
}
myString[O] = (unsigned char)mylndex; return (myStri ng) ;
There's no magic here" we just allocate a buffer large enough to hold the characters in the C string (up to a maximum of 255 characters) and the length byte of the Pascal string, copy the characters from the existing C string into the new buffer, and then finish up by prepending the length byte. Listing 12.11 defines the QTUtils_ConvertPascaIToCString function, which performs the reverse conversion. Listing 12.11 Converting a Pascal string into a C string. char *QTUtils ConvertPascaIToCString (StringPtr theString)
{
char short
*myString - malloc(theString[O] + 1); mylndex = O;
for (mylndex = O; mylndex < theString[Ol; mylndex++) myString[mylndex] = theString[mylndex + 1] ; myString[theString[O]] = ' \ 0 ' ; return (myStri ng) ;
The QuickTime Media Layer 3 6 5
Whenever we use these functions in our code, we need to make sure to free the buffer allocated by mall oc, by calling free once we're done using the string. Let's sum this up. Our preferred m e t h o d for working with strings will be to create all strings as C strings. W h e n we need a Pascal string, we'll explicitly create a new Pascal string by calling QTUtils_ConvertCToPascalString. To illustrate, reconsider these lines from Listing 12.5: if
(glsNeg) DrawStri ng ("\p-") ; else DrawString ("\p+") ;
We'll need to rewrite them now, like this" StringPtr mySign : NULL; i f (glsNeg) mySign = QTUtils ConvertCToPascaIString("-") ; else mySign = QTUtils ConvertCToPascaIString("+") ; DrawString (mySi gn) ; free (mySi gn) ; m
D
Updating the QTTimeCode sample code is left as an exercise for the reader.
Converting Data Types Occasionally we need to convert between similar Macintosh and W i n d o w s data types. One case that pops up often enough to warrant attention is the conversion between the Macintosh data type Rect and the W i n d o w s data type RECT. The fields of these structures have the same names, but in a Rect the fields are of type short, while in a RECT they are of type long. {Also, for what it's worth, the fields are not in the same order in these two structures.) QTML knows how to do this conversion, but it does not provide a public API for it; so I wrote the function OTFrame_ConvertMacToWinRect defined in Listing 12.12.
366
Chapter 12 2001A SpaceOdyssey
Listing
12.12 Converting a Macintosh rectangle into a Windows rectangle.
void QTFrame_ConvertMacToWinRect (Rect *theMacRect, RECT *theWinRect)
{
theWi nRect->top = (l ong) theMacRect->top; theWinRect->left = (long)theMacRect->left; theWi nRect->bottom = (l ong) theMacRect->bottom; theWi nRect->right = (long) theMacRect->ri ght;
We've b u m p e d into this function previously {see Chapter 10, "Word Is Out") but haven't discussed it explicitly. It's really rather simple, of course. {So much so that I'll leave the companion function QTFrame_ConvertWinXoMacRect as an easy exercise for the reader.} QTML does provide some useful functions for converting other kinds of structures. For instance, we can convert between Macintosh regions (of type Macaegion) and Windows regions (of type Region) by calling the MacaegionToNativeRegion and NativeRegionToMacRegion functions.
Handling Text Dialog boxes, both modal and modeless alike, often contain fields where the user can enter and edit text. The dialog boxes shown in Figures 12.2 and 12.3 use the Control Manager's edit text control. Some Macintosh applications use TextEdit for more complicated text support. QTML does not support TextEdit. If you need simple text entry and editing services, use the edit text control (in a Mac-style dialog box) or use the Windows native edit control (in a Windows window}. If you need more complicated text-editing services, you'll have to do a bit of programming.
Carbon Unlike QTML, Carbon is a porting layer. In particular, Carbon is a set of programming interfaces and a runtime library that together define a subset of Macintosh APIs that are supported both on classic Mac operating systems and on the new Mac OS X. By writing our code to conform to the Carbon standard, we can ensure that our compiled applications will run on both Mac platforms. In general, it's easier to port existing QuickTime code to Carbon than it is to port it to QTML. There are just a few issues we need to pay attention to
Carbon
367
w h e n reworking some existing Mac code to run on Carbon [that is, w h e n Carbonizing our application). First, we need to make sure that all the OS and Toolbox functions we call are part of the Carbon standard. This is because some existing functions have been dropped [usually in favor of better technologies). Second, we need to make sure that we use accessor functions w h e n e v e r necessary. This is because m a n y of the data structures that hitherto were public are now opaque; we cannot directly read or write the data in their fields. At times Carbon and QTML seem to be working at cross-purposes, but it turns out that it's fairly easy to support both Carbon and Windows w i t h a single set of source code files. In this section, I want to focus on the kinds of changes we need to make to our QuickTime-savvy code to get it to r u n on Carbon, while maintaining our Windows compatibility.
Accessing Fields of Data Structures On Carbon, m a n y of the key data structures used by the OS and Toolbox managers have been privatized (made opaque). For instance, in the not too distant past, the standard way of getting the m e n u ID of the m e n u associated with the m e n u handle myMenu was like this: myID = (**myMenu).menuID;
Nowadays, w h e n w e ' r e targeting Carbon, we must instead use the accessor function GetMenuID, like this: myID = GetMenuID(myMenu) ;
It turns out, however, that GetMenuID (like most of the new accessor functions) is not supported by QTML. So we might conditionalize our code, like this: # i f ACCESSOR CALLS ARE FUNCTIONS myID = GetMenuID(myMenu); #else myID = (**myMenu).menuID; #endif
Alternatively, we could just stick the following lines somewhere in one of our project's header files: # i f IACCESSOR CALLS ARE FUNCTIONS #define GetMenuID(mHdl ) (**mHdl) .menulD #endi f
368
Chapter 12 2001: A Space Odyssey
In this case, we can call GetMenulD w i t h o u t worrying w h e t h e r we're building a Carbon application or not. Which of these (or still other) options you adopt is largely a matter of taste, I suppose. Personally, I generally opt for the former approach. Partly that's because it's not always so easy to concoct a suitable #define. In an ideal world, the Apple-supplied header files should contain those defines, or else QTML should implement the accessor functions.
Replacing Unsupported Functions The really troubling problem in writing Carbon- and QTML-compatible code concerns Mac OS and Toolbox functions that have been dropped entirely from Carbon. A good case in point is the ICM function StandardGetFi ]eVreview. Not too terribly long ago, we used StandardGetFileVreview in both Mac and Windows code to elicit files from the user. StandardGetFi ]eVreview relies on the services of the Standard File Package, which is not supported on Carbon. So we need to rework our file-opening and file-saving code to use the Navigation Services APIs. In this case, I decided to create w r a p p e r functions that internally call either the Standard File Package or Navigation Services, depending on our target runtime environment. For instance, to elicit a file from the user, I wrote the OTFrame GetOneFileWithPreview function, shown in Listing 12.13. It
may be a bit lengthy, but it does the trick. Listing 12.13 Eliciting a file from the user. OSErr QTFrame_GetOneFiI eWithPrevi ew (short theNumTypes, QTFrameTypeListPtr theTypeLi st, FSSpecPtr theFSSpecPtr, void *theFilterProc)
{
#if TARGETOS WIN32 Standard Fi I eReply #endif #if TARGETOS MAC NavReplyRecord NavDial ogOptions NavTypeLi stHandl e NavEventUPP #endif OSErr D
myReply;
myRepI y; myDial ogOpti ons; myOpenLi st = NULL; myEventUPP = NewNavEventProc(QTFrame_HandleNavEvent) ; myErr = noErr;
i f (theFSSpecPtr == NULL) return (paramErr) ; / / deactivate any frontmost movie window QTFrame ActivateControl ler(QTFrame GetFrontMovieWindow(), false) ; m
Carbon
369
# i f TARGETOS WIN32 / / prompt the user for a f i l e StandardGet Fi I ePrevi ew( ( Fi I eFi I terUPP) theFi I terProc, theNumTypes, (ConstSFTypeListPtr)theTypeList, &myReply) ; i f ( ! myRepI y. sfGood) return (userCancel edErr) ; / / make an FSSpec record myErr = FSMakeFSSpec(myReply.sfFile. vRefNum, myReply.sfFile.parID, myReply.sfFile.name, theFSSpecPtr) ; #endif # i f TARGETOS MAC / / specify the options for the dialog box NavGetDefauI tDi aI ogOptions (&myDiaI ogOpti ons) ; myDial ogOpti ons. dial ogOpti onFl ags -= kNavNoTypePopup; myDialogOptions.dialogOptionFlags -= kNavAllowMultipleFiles; BlockMoveData(gAppName, myDialogOptions.clientName, gAppName[O] + i ) ; m
/ / create a handle to an 'open' resource myOpenLi st = (NavTypeListHandl e)QTFrame_CreateOpenHandl e( kApplicationSignature, theNumTypes, theTypeList) ; i f (myOpenList != NULL) HLock ( (Handl e) myOpenList) ; / / prompt the user for a f i l e myErr = NavGetFile(NULL, &myReply, &myDialogOptions, myEventUPP, NULL, (NavObjectFilterUPP)theFilterProc, myOpenList, NULL); i f ((myErr == noErr) && myReply.validRecord) { AEKeyword myKeyword; DescType myActual Type; Size myActuaISize = O; / / get the FSSpec for the selected f i l e i f (theFSSpecPtr i= NULL) myErr = AEGetNthPtr(&(myReply.selection), 1, typeFSS, &myKeyword, &myActuaIType, theFSSpecPtr, sizeof(FSSpec), &myActuaISize) ; NavDi sposeReply (&myReply) ;
370
Chapter 12 2001: A Space Odyssey
i f (myOpenList l= NULL) { HUnlock ( (Handl e)myOpenList) ; Di sposeHandle ( (Handl e)myOpenList) ;
}
Di sposeNavEventUPP(myEventUPP) ; #endi f return (myErr) ;
W o r k i n g with Universal Procedure Pointers There is one set of unsupported functions that is relatively easy to deal with, namely, the three functions NewRoutineDescriptor, Di sposeRoutineDescriptor, and CallUniversalProc. On classic Macintosh systems, a universal procedure pointer (UPP) is a pointer to a routine descriptor, which is a structure that occupies m e m o r y (and hence must be allocated and disposed of). On Carbon, the UPP data type is opaque and might or might not require memory allocation. So we need to use a new creation and deletion function for each specific type of UPP we want to use. For instance, in Listing 12.13, we created a UPP for a Navigation Services event procedure by calling NewNavEventProc. To dispose of this UPP, we call DisposeNavEventUPP. Similarly, we can create a UPP for a modal dialog event filter by calling NewModalFi 1terProc. To dispose of this UPP, we call Di sposeModal Fi 1terUPP. If we ever needed to call this procedure ourselves, we would use the function InvokeModal Fi I terUPP. The good news here is that the header files for Macintosh APIs provide definitions of these new UPP functions for non-Carbon targets. For instance, the header file Dial ogs.h contains some lines like this" #i f OPAQUE_UPP_TYPES EXTERN API (void) DisposeModaIFilterUPP(ModaIFilterUPP userUPP) ; #else #define Di sposeModalFi I terUPP(userUPP) Di sposeRouti neDescri ptor(userUPP) #endif D
This means that we can revise our code to use (for instance) DisposeModal FilterUPP, and the resulting source code will compile and run on Windows, classic Macintosh, and Mac OS X.
Carbon 371
Conclusion The QuickTime Media Layer is a rock-solid implementation of key parts of the Macintosh Operating System and the Macintosh User Interface Toolbox for Windows computers. We've considered a n u m b e r of changes that we might need to make to our existing QuickTime code to get it working on Windows. The changes are, all things considered, relatively straightforward. We occasionally need to byte-swap data read from or written to a file. We need to ferret out the Mac APIs that have the same names on Mac and Windows and rename the Mac versions. We need to explicitly open our application's resource fork on W i n d o w s if we use any Mac-style resources. And we need to install a callback procedure if we want to w o r k with modeless dialog boxes on Windows. Otherwise, things w o r k pretty m u c h the same on both platforms. QTML is not a porting layer, but it is amazingly good at supporting a large set of Mac APIs. There is, unfortunately, no definitive documentation on which functions are available and which are not. My advice is to experiment; if a Mac function compiles, links, and runs on Windows, that's great.
372
Chapter 12 2001: A Space Odyssey
Honey, I Shrunk the Kids
Introduction In Chapter 6, "Doug's 1st Movie," when we built our very first QuickTime movie, we used a couple of Image Compression Manager functions to compress each video frame so that the frame [and hence the entire movie) took up less space on disk. The size reduction was significant: simply adding 100 uncompressed frames to the movie would have resulted in a movie file that was about 12 megabytes in size. Using JPEG compression, we were able to reduce the final movie file size to about 470 Kbytes. In that chapter, however, we cut some corners by hard-coding the compression type when we called GetMaxCompressionSize and Compresslmage. It would have been nice to provide the user with a choice of compression algorithms and indeed perhaps even an indication of what any particular compression algorithm would do to the penguin images. Happily, QuickTime makes this very easy to do, by supplying the standard image compression dialog component. We can use this component to perform two main tasks. First, as the name suggests, we can have it display a dialog box in which the user can adjust compression settings for a single image. Figure 13.1 shows the
standard image compression dialog box. The standard image compression dialog box contains a pop-up menu that lists the available image compressors. It also contains a pop-up menu that lists the available pixel depths supported by the selected compressor. The dialog box also contains a slider control for adjusting the image quality. As the user varies the compressor, pixel depth, or image quality, the standard image compression dialog component adjusts the thumbnail picture to show what the image would look like if compressed using the selected settings. The second main task that the standard image compression dialog component can perform is to compress the image. That is to say, not only can it retrieve the desired compression settings from the user, but it can also do
373
Figure 13.1 The standard image compression dialog box. the actual compression for us (thereby saving us from having to call GetMaxCompressionSize and CompressImage). For this reason, the component is sometimes also called the standard compression component. Figure 13.2 shows the result of using the standard image compression dialog component to compress our penguin picture using the PNG image compressor at 16 levels of grayscale and the highest available quality. In this chapter, we'll see how to use the standard image compression dialog component to elicit compression settings from the user and to compress images using the settings selected by the user. We'll also see how to use the standard image compression dialog component to compress a sequence of images (for example, the sequence of images that make up our penguin movie). We'll begin by taking a more focussed look at compression itself. While the basic idea is straightforward, there are a handful of concepts we'll need to understand before we can start using the standard image compression dialog component. Then we'll spend the rest of this chapter investigating the compression-related parts of the sample application QTCompress. The Test menu for QTCompress is shown in Figure 13.3; as you can see, it contains only one menu item, which compresses the image or sequence of images in the frontmost window.
374
Chapter 13 Honey, I Shrunk the Kids
Figure 13.2 The penguin picture compressed with PNG grayscale.
Figure 13.3 The Test menu of QTCompress.
Compression Compression is the process of reducing the size of some collection of data, presumably without unduly compromising the integrity of that data. The basic goal, of course, is to be able to store the data in less space and to use less bandwidth when transferring the data over a network. Particularly for multimedia content like large color images, movies, and sounds, uncompressed data (also known as raw data) simply takes up too much space on disk or too much time to transfer over a network. It's almost always better to store and transfer compressed data, which is then decompressed during playback. In QuickTime, compression and decompression are handled by components called codecs (which as we've seen before is short for compressor/ decompressor). The available codecs effectively define the kinds of compressed data that QuickTime can handle. Apple has written a large number of codecs itself and also licensed some other codecs from third-party developers. Ideally, it would be nice if QuickTime supplied both a compressor and a decompressor for every kind of data that it can handle, but sadly that
Compression 375
isn't the case. For instance, QuickTime can decompress and play MP3 files, but it does not currently include a component that can compress sound data into the MP3 format. For the present, we'll be concerned primarily with compression and decompression of images and sequences of images. In this case, there are two basic kinds of compression: spatial compression and temporal compression. Spatial compression is a means of compressing a single image by reducing redundant data in the image. For instance, our penguin picture has large areas of pure white; a good spatial compressor would encode the image so as to avoid having to store a 32-bit RGB value for every one of those white pixels. Exactly how the encoding is accomplished varies from compressor to compressor. Temporal compression is a means of compressing a sequence of images by comparing two adjacent frames and storing only the differences between the two frames. It turns out that many common sorts of video change very little from frame to frame, so a significant size reduction can be achieved by storing a full frame of the video and then the subsequent differences to be applied to that frame in order to reconstruct the original image sequence. In QuickTime, the full frame of video is called a key frame, and the subsequent frames that contain only the differences from previous frames are called difference frames, or delta frames. (Other media technologies use other nomenclature. Key frames are also called intraframes, and difference frames are also called interframes. MPEG calls key frames I-frames and has two sorts of difference frames, B-frames and P-frames.) In theory, a temporally compressed movie could consist of a single key frame followed by a large number of difference frames. But in practice, key frames are interspersed throughout the movie at predetermined intervals. This is because, to be able to draw any particular frame, the preceding key frame and all difference frames following that key frame {up to the frame to be drawn) must be processed. It would be prohibitively slow to jump to a random spot in a movie, or play a movie backwards, if it consisted of a single key frame and a bunch of difference frames. The maximum number of frames that can occur before a key frame is inserted is the key frame rate. A compressor may insert key frames more often than at the specified key frame rate, however (for instance, at a scene change, where there is very little similarity between one frame and the following frame). Note that spatial and temporal compression are not competing forms of compression. Indeed, most QuickTime movies employ both spatial and temporal compression, since the key frames of a movie are typically spatially compressed images. Note also that the use of temporal compression forces us to revise our understanding of the data stored in a QuickTime movie file. Up to now, we've tended to think of the movie data as a sequence of images. Now we see that it's more accurate to think of the movie data as a sequence
376
Chapter 13 Honey, I Shrunk the Kids
of images (key frames) and changes to those images (difference frames). Only at playback time (that is, after the movie data is decompressed) do we get an actual series of images.
Compressing Images The sample applications that we've developed so far in this book can open both QuickTime movie files and image files and display them in windows on the screen. When the user selects the Compress menu item in the Test menu, QTCompress executes this code: case IDM COMPRESS: i f (QTFrame_Is ImageWindow(myWindow)) QTCmpr_CompressImage(myWindowObject) ; else QTCmpr_CompressSequence(myWindowObject) ; myIsHandled = true; break;
As you can see, if the frontmost window contains an image, then QTCompress calls the function QTCmpr_CompressImage [which we'll consider in this section); otherwise, it calls QTCmpr_CompressSequence (which we'll consider in the next section). The QTCmpr_CompressImage function is built mainly around two routines provided by the standard image compression dialog component, SCRequestImageSettings and SCCompresslmage. SCRequestlmageSettings displays and manages the standard image compression dialog box (see Figure 13.1), and SCCompressImage performs the actual compression of the image data into a new buffer. (As you've probably guessed, all functions provided by the standard image compression dialog component begin with the letters "SC".I
Getting the Image Pixel Map The first thing we need to do when compressing an image is to draw it into an offscreen graphics world. We'll use the pixel map associated with that offscreen graphics world in two ways. First, we'll pass it to the SCSetTestImageVixMao function to set the thumbnail image in the image compression dialog box. Then, later on, we'll pass it to SCCompressImage as the source image to be compressed. As we've seen in earlier chapters, we can use graphics importers to open and draw image files. In fact, we already have an instance of a graphics importer component associated with the image file; it's the one we use to
Compressing Images 377
draw the image into the window on the screen--namely, (**theWindowObject) .fGraphicslmporter. But here we're going to create a
new
graphics
importer instance to draw the image into the offscreen graphics world. This is because we'll want to use the existing graphics importer to redraw the image in the onscreen window inside of the modal-dialog event filter procedure QICmpr_Fi ] terProc (defined later). It might in fact be possible to cleverly juggle the graphics importer's graphics world (using firaphicsImportSetSWorl d), but I never managed to get that strategy to work properly. So let's create a new graphics importer instance for the image to be compressed: myErr = GetGraphicslmporterForFile( &(**theWindowObject). fFi leFSSpec, &myImporter) ; i f (myErr l= noErr) goto bai I ; myErr = Graphi cs ImportGetNatura] Bounds(myImporter, &myRect); i f (myErr ! = noErr) goto bai I ;
Now that we know the size of the image, we can use this code to create the requisite offscreen graphics world: myErr = QTNewGWorld(&mylmageWorld, O, &myRect, NULL, NULL, k I CMTempThenAppMemory); i f (myErr ! = noErr) goto bai I ; / / get the pixmap of the GWorld; we'll lock the pixmap, just to be safe myPixMap = GetGWorldPixMap(mylmageWorld) ; i f ( ! LockPi xel s (myPixMap)) goto bai I ;
Finally, we need to draw the image into myImageWorld. GraphicslmportSetGWorld(mylmporter, (CGrafPtr)mylmageWorld, NULL); Graphics ImportDraw (mylmporter) ;
At this point, the offscreen graphics world myImageWorld contains the image to be compressed.
Setting the Test Image Now we want to display the standard image compression dialog box, to get the user's desired compression settings. To do this, we need to open an instance of the standard image compression dialog component, like so:
3178 Chapter 13 Honey, I Shrunk the Kids
myComponent = OpenDefaul tComponent (StandardCompressionType, StandardCompres s i onSubType) ;
Before we call the SCRequestlmageSettings function to display the dialog box on the screen, we need to set the thumbnail picture (called the test image) that is displayed in the top-right part of the dialog box. We do this by calling the SCSetTest ImagePi xMap function: SCSetTestlmagePixMap(myComponent, myPixMap, NULL, scPreferScaling) ;
Here, the first two parameters are the instance of the standard image compression dialog component and the pixel map that contains the image. The third parameter is a pointer to a Rect structure that specifies the area of interest in the pixel map that is to be used as the test image. Passing the value NULL means to use the entire pixel map as the test image, suitably reduced into the 80-by-80 pixel area in the dialog box. The fourth parameter indicates how the image reduction is to occur; it can be any of these constants: enum { scPreferCropping scPreferScaling scPreferScalingAndCropping scDontDetermineSettingsFromTestlmage
};
= = = =
I << O, 1 << 1, scPreferScaling I scPreferCropping, 1 << 2
You can also pass the value 0 in the fourth parameter to request the component's default method of displaying the test image, which is currently a combination of scaling and cropping. Personally, I prefer just scaling the image to fit into the available space, so I've passed the value scPreferScal ing.
Installing Extended Procedures We're almost ready to call SCRequestlmageSettings to display the dialog box. We need to do just one more thing to configure the dialog box, namely, install one or more callback procedures that extend the basic functionality of the standard image compression dialog component. (These are therefore called extended procedures.) We install these callback procedures by calling the SCSetInfo function with the scExtendedProcsType selector: SCSetlnfo(theComponent, scExtendedProcsType, &gProcStruct) ;
Here, gProcStruct is an extended procedures structure, of type SCExtendedProcs, which is defined like this:
Compressing Images 379
struct SCExtendedProcs { SCModalFi I terUPP SCModalHookUPP long Str31
};
fi I terProc; hookProc; refcon; customName;
The filterProc field specifies a modal-dialog event filter function to handle events that are not handled by the standard image compression dialog component itself. As we've done in the past, we'll provide a filter function that looks for update events for our application's windows and redraws those windows accordingly. Listing 13.1 shows our custom filter function.
Listing 13.1 Filtering events in the image compression dialog box. static PASCAL_RTNBoolean QTCmpr_FiI terProc (DialogPtr theDialog, EventRecord *theEvent, short *theltemHit, long theRefCon)
(
#pragma unused(theltemHit, theRefCon) Boolean myEventHandled = false; WindowRef myEventWi ndow = NULL; WindowRef myDialogWindow = NULL; # i f TARGET API MAC CARBON myDialogWindow = GetDialogWindow(theDialog) ; #else myDial ogWindow = theDi al og; #endif ~
m
switch (theEvent->what) { case updateEvt 9 / / update the specified window, i f i t ' s behind the modal dialog box myEventWi ndow = (WindowRef)theEvent->message; i f ((myEventWindow i= NULL) && (myEventWindow != myDialogWindow)) { # i f TARGET OS MAC QTFrame_HandleEvent (theEvent) ; #endi f myEventHandled = false; m
}
}
break;
return (myEventHandled) ;
3180 Chapter 13 Honey, I Shrunk the Kids
This is a fairly typical event filter function. It looks for update events that are not destined for the dialog box and sends t h e m to the f r a m e w o r k ' s event-handling function. Notice that this step isn't necessary on Windows, w h e r e redraw messages are sent directly to the w i n d o w procedure of the affected window. We can also install a hook function, w h i c h is called w h e n e v e r the user chooses {or "hits") an item in the dialog box. We can then intercept those hits and handle them in any way we like. A typical w a y to use the hook function is in connection with a custom button in the standard image compression dialog box. Figure 13.4 shows the dialog box with a new button labeled "Defaults." We install this custom button by specifying a n a m e for the button in the customName field of the extended functions structure. We can do that w i t h these two lines of code: StringPtr myButtonTitle = QTUtils ConvertCToPascalString("Defaults") ; BlockMove(myButtonTitle, gProcStruct.customName, myButtonTitle[O] + i ) ;
Then we can handle user clicks on this custom button inside our hook function, QTCmpr_ButtonProc, defined in Listing 13.2.
Figure 13.4 The standard image compression dialog box with a custom button.
Compressing Images 381
Listing 13.2 Intercepting events in a hook function. static PASCAL_RTNshort QTCmpr_ButtonProc (DialogPtr theDialog, short theltemHit, void *theParams, long theRefCon)
{
#pragma unused(theDialog) / / in this sample code, we'll have the settings revert to their default values / / when the user clicks on the custom button i f (theltemHit == scCustomItem) SCDefauI tPi xMapSetti ngs (theParams, (Pi xMapHandle) theRefCon, fa I se) ; / / always return the item passed in return (theItemHi t) ;
This hook function is extremely simple; it just looks for hits on the custom button [signaled by the constant scCustomItem) and then calls the SCOefaultPixMapSettings function to reset the dialog box to its default values. Notice that the reference constant passed to our hook function (in the theRefCon parameter) is expected to be a handle to the pixel map we created earlier. We install this reference constant by setting the refcon field of the extended procedures structure. Listing 13.3 shows our definition of the QTCmpr Instal 1ExtendedProcs function, which we use to set up our extended procedures.
Listing 13.3 Installing the extended procedures. static void QTCmpr_InstalIExtendedProcs (Componentlnstance theComponent, long theRefCon)
{
StringPtr myButtonTitle = QTUtil s ConvertCToPascaIString(kButtonTitle) ; / / the modal-dialog f i l t e r function can be used to handle any events that / / the standard image compression dialog handler doesn't know about, such / / as any update events for windows owned by the application gProcStruct, fi I terProc = NewSCModalFi I terUPP(QTCmpr_Fi I terProc) ; #if USE CUSTOMBUTTON / / the hook function can be used to handle clicks on the custom button gProcStruct, hookProc = NewSCModalHookUPP(QTCmpr_ButtonProc) ; / / copy the string for our custom button into the extended procs structure BlockMove(myButtonTitle, gProcStruct.customName, myButtonTitle[O] + 1); #else gProcStruct .hookProc = NULL; gProcStruct.customName[O] = O; #endif
382
Chapter 13 Honey, I Shrunk the Kids
/ / in this example, we pass the pixel map handle as a reference constant gProcStruct.refcon = theRefCon; / / set the current extended procs SCSetlnfo(theComponent, scExtendedProcsType, &gProcStruct); free (myButtonTi tl e) ;
You'll notice that we've used the compiler flag USE_CUSTOM_BUTTONto indicate whether we want to install a custom button in the standard image compression dialog box. Some image compressors want to install an Options button in that dialog box, and our custom button would prevent them from doing so. (We will see a dialog box that contains an Options button shortly in Figure 13.5.) For this reason, we usually won't install a custom button. But you should at least know how to do so. Finally, we can call QTCmpr_InstallExtendedProcs to install our extended procedures and then SCRequestImageSettings to display the dialog box. i f (gUseExtendedProcs) QTCmpr_Instal l ExtendedProcs(myComponent, (long)myPixMap); myErr = SCRequestImageSetti ngs (myComponent) ;
Compressing the Image If the user chooses the Cancel button in the standard image compression dialog box, then SCRequestImageSettings returns the value scUserCancelled. Otherwise, if SCRequestImageSettings returns noErr, we want to go ahead and compress the image. Thankfully, we can do this with a single call to the function SCCompressImage. myErr = SCCompressImage(myComponent, myPixMap, NULL, &myDesc, &myHandle);
SCCompressImagecompresses the specified pixel map using the current settings of the specified standard image compression dialog component. It allocates storage for the compressed image and returns a handle to that storage in the fifth parameter (myHandle). It also returns an image description in the fourth parameter (myOesc). We can write the compressed data into a new file by calling the application function QTCmpr_PromptUserForDiskFileAndSaveCompressed, passing in the compressed data and the image description. (See the file QTCompress.c in this chapter's source code for the definition of this function.) Listing 13.4 shows our complete definition of QTCmpr_CompressImage.
Compressing Images :383
Listing 13.4 Compressingan image. void QTCmpr_Compresslmage (WindowObject theWindowObject) Rect Graph i cs ImportComponent Component Instance GWorldPtr PixMapHandl e ImageDescri pt i onHandl e Handle OSErr
myRect; mylmporter = NULL; myComponent = NULL; mylmageWorld = NULL; myPixMap = NULL; myDesc = NULL; myHandl e = NULL; myErr = noErr;
i f (theWindowObject == NULL) return; myErr = GetGraphicslmporterForFi le(&(**theWindowObject), fFi leFSSpec, &mylmporter) ; i f (myErr ! = noErr) goto bai I ; myErr = GraphicslmportGetNatural Bounds(mylmporter, &myRect) ; i f (myErr l= noErr) goto bai I ; / / create an offscreen graphics world and draw the image into i t myErr = QTNewGWorld(&mylmageWorld, O, &myRect, NULL, NULL, kICMTempThenAppMemory); i f (myErr ! = noErr) goto bai I ; / / get the pixmap of the GWorld; we'll lock the pixmap, just to be safe myPixMap = GetGWorldPi xMap(mylmageWorl d) ; i f (! LockPi xel s (myPixMap)) goto bai I ; / / set the current port and draw the image GraphicslmportSetGWorld(mylmporter, (CGrafPtr)mylmageWorld, NULL) ; Graphi cs ImportDraw(mylmporter) ; / / open the standard compression dialog component myComponent = OpenDefaultComponent(StandardCompressionType, StandardCompressionSubType) ; i f (myComponent == NULL) goto bai I ;
384
Chapter 13 Honey, I Shrunk the Kids
/ / set the picture to be displayed in the dialog box SCSetTestlmagePixMap(myComponent, myPixMap, NULL, scPreferScaling); / / install the custom procs, i f requested i f (gUseExtendedProcs) QTCmpr_Instal l ExtendedProcs (myComponent, (long)myPixMap) ; / / request image compression settings from the user myErr = SCRequestImageSetti ngs (myComponent) ; i f (myErr == scUserCancelled) goto bai I ; / / compress the image myErr = SCCompresslmage(myComponent, myPixMap, NULL, &myDesc, &myHandle); i f (myErr l= noErr) goto bai I ; / / save the compressed image in a new f i l e QTCmpr_PromptUserForDiskFi I eAndSaveCompressed(myHandle, myDesc); bail: i f (gUseExtendedProcs) QTCmpr RemoveExtendedProcs() ; i f (myPixMap != NULL) i f (GetPixelsState(myPixMap) & pixelsLocked) Unl ockPi xel s (myPixMap) ; i f (mylmporter ! = NULL) Cl oseComponent(myImporter) ; i f (myComponent i= NULL) Cl oseComponent(myComponent) ; i f (myDesc ! = NULL) Di sposeHandle( (Handl e)myDesc) ; i f (myHandle ! = NULL) Di sposeHandl e (myHandle) ; i f (mylmageWorld l: NULL) Di sposeGWorld (mylmageWorld) ;
Compressing Images 385
Restricting Compressor Types Occasionally, it's useful to restrict the components listed in the pop-up menu of available compressor types in the standard image compression dialog box to one or several preferred components. We can do this quite easily by calling SCSetlnfo with the scCompressionListType selector, as shown in Listing 13.5. Listing 13.5 Allowing only one compressor type. Handle
myType = NULL;
myType = NewHandle(sizeof(OSType)) ; i f (myType ! = NULL) { *(OSType *)*myType = kJPEGCodecType; SCSetlnfo(myComponent, scCompressionListType, &myType); Di sposeHandl e (myType) ;
}
The data passed to SCSetInfo for the scCompressionListType selector is the address of a handle that holds one or more compressor types. If we insert this code into the OTCmpr_CompressImage function just before we call SCRequestImageSettings, we'll see that the pop-up menu lists only the JPEG compressor.
Compressing Image Sequences Compressing a sequence of images is not fundamentally different from compressing a single image. We'll need to display the standard image compression dialog box, as before, to get the user's desired compression settings. Then, however, instead of compressing a single image, we'll need to loop through all the images in the sequence and compress each one individually. We'll obtain our sequence of images by extracting the individual frames from an existing QuickTime movie, and then we'll write the compressed images into a new QuickTime movie. (In effect, we'll be converting a QuickTime movie from one compression scheme to another; this operation is often called transcoding.) So we'll have the added overhead of creating a new QuickTime movie, track, and media, and of adding samples to the media by calling AddMediaSample. We've done this kind of thing numerous times before, so that part of the code shouldn't slow us down too much. When we want to compress a sequence of images, we need to call SCRequestSequenceSettings instead of SCRequestSettings. The dialog box that it displays is shown in Figure 13.5; as you can see, it contains an additional
386
Chapter 13 Honey, I Shrunk the Kids
Figure 13.5 The standard image compression dialog box for an image sequence.
pane of controls for specifying the number of frames per second, the key frame rate, and the maximum data rate (the number of bytes per second that can be processed). When configuring the dialog box and when responding to its dismissal, we'll need to add code to handle this additional information. So let's get started.
Getting the Image Sequence As mentioned previously, we're going to obtain our sequence of images by reading the individual frames from the video track of a QuickTime movie. (Let's call this the source movie.) We can get the source movie by reading the r field of the window object record, and we can get the source movie's video track by calling the GetMovielndTrackType function: mySrcMovie : (**theWindowObject).fMovie; i f (mySrcMovie : : NULL) goto bai I ;
Compressing Image Sequences 387
mySrcTrack = GetMovieIndTrackType(mySrcMovie, 1, VideoMediaType, movi eTrackMedi aType) ; i f (mySrcTrack == NULL) goto bai I ;
To make things look a bit cleaner, we don't want the movie to be playing while we are compressing its frames into a new movie, so we call SetMovieRate to stop the movie. We also need to keep track of the current movie time, since we'll be changing it as we move from frame to frame through the movie. SetMovieRate(mySrcMovie, (Fixed)OL) ; myOrigMovieTime = GetMovieTime(mySrcMovie, NULL);
Later, w h e n w e ' r e done recompressing the frames of the movie, we'll reset the movie time to this saved value. Finally, we need to know how m a n y video frames are in the source movie, so that we know (for instance) how m a n y iterations our loop should make. OTCmpr_CompressSequence includes this line of code for counting the
frames of the source movie: myNumFrames = QTUti I s GetFrameCount(mySrcTrack) ;
There are several methods we could use to determine how m a n y frames the source movie contains. Probably the best method is just to step through the interesting times in the movie using the GetTrackNextInterestingTime function, as shown in Listing 13.6.
Listing 13.6 Counting the frames in a movie. long QTUtils GetFrameCount (Track theTrack)
{
m
long short TimeVal ue
myCount = -1; myFlags; myTime = O;
i f (theTrack == NULL) goto bai I ; / / we want to begin with the f i r s t frame (sample) in the track myFlags = nextTimeMediaSample + nextTimeEdgeOK; while (myTime >: O) { myCount++;
388
Chapter 13 Honey, I Shrunk the Kids
/ / look for the next frame in the track; when there are no more frames, / / myTime is set t o - 1 , so we'll exit the while loop GetTrackNextlnterestingTime(theTrack, myFlags, myTime, fixedl, &myTime, NULL); / / after the f i r s t interesting time, don't include the time we're currently at myFlags = nextTimeStep; bail: return (myCount) ;
}
For more discussion of GetTrackNext I nteres t i ngTi me, see Chapter 10, "Word Is Out."
Configuring the Standard Image Compression Dialog Component As before, we need to open an instance of the standard image compression dialog component and configure the initial settings of the dialog box. Opening an instance of the component uses the same code we used in the case of a single image: myComponent = OpenDefaultComponent(StandardCompressionType, StandardCompressi onSubType) ;
To configure the settings in the dialog box, we first want to turn off the Best Depth menu option in the pixel depth pop-up menu. This is because we are going to draw the movie frames into a 32-bit offscreen graphics world, regardless of the pixel depth of the original source images. A better approach might be to determine the maximum bit depth used in the source images {by looping through the video sample descriptions of the video frames} and then create an offscreen graphics world of that depth. {This refinement, of course, is left as an exercise for the reader.) We can disable the Best Depth option using this code: SCGetlnfo(myComponent, scPreferenceFlagsType, &myFlags); myFlags &= -scShowBestDepth; SCSetlnfo(myComponent, scPreferenceFlagsType, &myFlags);
Next, we want to allow the user to leave the frame rate field blank (in which case the compression component will preserve the original frame durations of the source movie). To do this, we need to specify that 0 is an acceptable value in that field. We do that by executing these lines of code:
Compressing ImageSequences 389
SCGetlnfo(myComponent, scPreferenceFl agsType, &myFlags) ; myFlags I = scAllowZeroFrameRate; SCSetlnfo(myComponent, scPreferenceFlagsType, &myFlags) ;
If the user enters a n u m b e r in the frame rate field, we'll use that n u m b e r as the new sample rate for the destination movie.
Setting the Test Image Before we display the compression settings dialog box to the user, we w a n t to set the test image. In the present case, however, we have an entire sequence of images to handle, not just a single image. Which of those images shall we select as the test image? Let's select the movie poster image, on the assumption that that image is representative of the content of the entire sequence of images (that is, of the source movie itself). So we can call GetMoviePosterPi ct to get a PicHandle to the test image: myPicture = GetMoviePosterPict (mySrcMovie) ;
Then we can get the size of the poster image and create an offscreen graphics world large enough to hold that image: GetMovieBox(mySrcMovie, &myRect) ; myErr = NewGWorld(&mylmageWorld, 32, &myRect, NULL, NULL, OL);
And, as before, we'll lock the pixel map of that graphics world: myPixMap = GetGWorldPixMap(mylmageWorld) ; i f (! LockPi xel s (myPi xMap)) goto bai I ;
Next we want to draw the poster image into the offscreen graphics world. Since we've got a handle to a QuickDraw picture, we can use the 9rawPicture function to draw the picture. First, however, we need to make sure to set the current graphics world to our new offscreen graphics world and to erase the destination graphics world as follows: GetGWorld(&mySavedPort, &mySavedDevice) ; SetGWorld(mylmageWorld, NULL); EraseRect (&myRect) ; DrawPi cture (myPi cture, &myRect) ; Ki I l Picture (myPi cture) ; SetGWorld(mySavedPort, mySavedDevice) ;
390
Chapter 13 Honey, I Shrunk the Kids
Finally, we are .ready to call SCSetTestImagePixMap to set the test imagei SCSetTestlmagePixMap(myComponent, myPixMap, NULL, scPreferScaling) ;
Displaying the Compression Settings Dialog Box Once again, we have a couple of things still to do before we can display the standard image compression dialog box. For one thing, we need to install the extended procedures; here, we can use exactly the same application function as in the single-image case: i f (gUseExtendedProcs) QTCmpr_Instal l ExtendedProcs (myComponent, (l ong)myPixMap) ;
Next, we want to set some default settings for the dialog box. The standard image compression dialog component can examine the pixel map that we just created and derive some sensible default settings based on the characteristics of that image. So let's take advantage of that capability: SCDefaul tPixMapSettings (myComponent, myPixMap, true) ;
Also, we want to clear out whatever default frame rate was selected by the standard image compression dialog component. As we have discussed, we would like to use the frame rate O, indicating that the frame rate of the source movie should be used. (The user is free to change this rate, but at least we want the default value to be 0.) We can first retrieve and then reset the current temporal settings of the component: myErr = SCGetlnfo(myComponent, scTemporaISettingsType, &myTimeSettings) ; i f (myErr I= noErr) goto bai I ; myTimeSettings, frameRate = O; SCSetlnfo(myComponent, scTemporaISettingsType, &myTimeSettings) ; SCGetlnfo and SCSetlnfo expect temporal settings to be stored in a structure of type SCTemporalSettings, defined like this: struct SCTemporalSettings CodecQ Fixed long
};
{
temporaI Qual i ty; frameRate; keyFrameRate;
Compressing ImageSequences 391
Finally, we're ready to call SCRequestSequenceSettings to display the standard image compression dialog box: myErr = SCRequestSequenceSetti ngs (myComponent) ;
Adjusting the Sample Count We have successfully displayed the standard image compression dialog box and the user has selected his or her desired compression settings. In the single-image case, we could finish up rather quickly, by immediately calling SCCompressImage and then saving the compressed data into a new file. In the current case, however, we still have a good bit of work left to do. We need to retrieve the temporal settings--which may indicate a new frame rate for the destination movie--and configure the destination movie accordingly. Then we need to step through the frames of the source movie and compress each frame in the movie. We'll begin by first retrieving the temporal settings selected by the user: myErr = SCGetInfo(myComponent, scTemporaISettingsType, &myTimeSettings) ;
If the user wants to change the frame rate of the movie (as indicated by a nonzero value in the frameRate field of the temporal setting structure myTimeSettings), then we need to calculate the number of frames in the destination movie and the duration of the destination movie. We can do that like this: if
(myTimeSettings.frameRate ! = O) { long myDuration = GetMovieDuration(mySrcMovie) ; long myTimeScale = GetMovieTimeScale(mySrcMovie) ; float myFloat = (float)myDuration * myTimeSettings.frameRate; myNumFrames : myFloat / myTimeScale / 65536; i f (myNumFrames : : O) myNumFrames = 1;
Creating the Target Movie Suppose now that myFil e is a file system specification for the destination movie file (perhaps we called our framework function OTFrame_PutFile to elicit that file specification from the user). At this point, we need to create the destination movie file and movie, as shown in Listing 13.7.
392
Chapter 13 Honey, I Shrunk the Kids
Listing 13.7 Creating a new movie file and movie. myErr = CreateMovieFile(&myFile, sigMoviePlayer, smSystemScript, createMovieFi leDeleteCurFi le [ createMovieFi leDontCreateResFi le, &myRefNum, &myDstMovie) ; i f (myErr ! = noErr) goto bai I ; / / create a new video movie track with the same dimensions as the entire source movie myDstTrack = NewMovieTrack(myDstMovie, (Iong)(myRect.right - myRect.left) << 16, (long) (myRect.bottom - myRect.top) << 16, kNoVolume); i f (myDstTrack == NULL) goto bai I ; / / create a media for the new track with the same time scale as the source movie; / / because the time scales are the same, we don't have to do any time scale conversions myDstMedia = NewTrackMedia(myDstTrack, VIDEO_TYPE, GetMovieTimeScale(mySrcMovie), O, 0); i f (myDstMedia == NULL) goto bai I ;
We of course want to copy the movie user data and other settings from the source movie to the destination movie" CopyMovieSetti ngs (mySrcMovie, myDstMovie) ;
Also, we want to set the movie matrix of the destination movie to the identity matrix and clear out the movieclip region. This is because the process of recompressing the movie transforms and composites all the video tracks into a single, untransformed video track: Set Identi tyMatri x (&myMatri x) ; SetMovieMatrix(myDstMovie, &myMatrix) ; SetMovieCl ipRgn(myDstMovie, NULL);
We want the quality of the image drawn into the offscreen graphics world to be as good as possible, so we'll call SetMoviePlayHints with the h i n t s Hi ghQual i ty constant: SetMoviePlayHints(mySrcMovie, hintsHighQual i t y , hintsHighQual i t y ) ;
This ensures that QuickTime will favor image quality over image-rendering speed.
Compressing Image Sequences 393
Finally, since we're abou~ to start adding compressed video samples to the destination movie, we need to call Begi nMedi aEdi ts: myErr = Begi nMedi aEdi ts (myDstMedia) ;
Compressing the Image Sequence We are now ready to start compressing the frames of the source movie into the destination movie. We are going to step through the source movie, d r a w the current frame into our offscreen graphics world, compress that data, and then add the compressed data as a video sample to the destination movie. To start things off, we call SCCompressSequenceBegin: myErr = SCCompressSequenceBegin(myComponent, myPixMap, NULL, &myImageDesc);
SCCompressSequenceBegin initiates a compression sequence, using the current settings of the specified component instance and the characteristics of the specified pixel map. The third parameter is a pointer to a Rect structure that indicates what portion of the pixel map we're going to be compressing; the value NULLindicates that we want to compress the entire pixel map. The fourth parameter is a pointer to an image description. The standard image compression dialog component will allocate storage for that description and fill in its fields w h e n it compresses some data for us. In turn, we shall use that image description w h e n we call AddMediaSample. Next we need to erase the offscreen graphics world that we're going to be drawing movie frames into and set the source movie to draw its frames into that graphics world: SetGWorld(mylmageWorld, NULL); EraseRect (&myRect) ; SetMovieGWorld(mySrcMovie, mylmageWorld, GetGWorldDevice(mylmageWorld)) ;
From now on, until we reset the movie graphics world, calling MoviesTask on the source movie will cause the current movie frame to be d r a w n into myImageWorl d.
So let's start looping through the frames of the source movie. We begin by setting a variable that holds the current movie time to the beginning of the movie and by retrieving the duration of the source movie: myCurMovieTime = O; mySrcMovieDuration = GetMovieDuration(mySrcMovie) ;
394
Chapter 13 Honey, I Shrunk the Kids
The remainder of our code in this section will occur inside of a for loop that iterates through all the frames of the source movie: for (myFrameNum = O; myFrameNum < myNumFrames; myFrameNum++) { / / get a frame, compress i t , and add i t to the new movie
}
The first thing we want to do inside this loop is get the next frame of the source movie. If we are not resampling the movie {that is, changing the frame rate), we can use GetMovieNextlnterestingTime to step us forward in the
movie; otherwise, we need to step forward by the appropriate amount, based on the desired new frame rate. Listing 13.8 shows our code for this step. Listing 13.8 Getting the next frame of the source movie. i f (myTimeSettings.frameRate) { myCurMovieTime = myFrameNum * mySrcMovieDuration / (myNumFrames - 1); myDuration = mySrcMovieDuration / myNumFrames; } else { OSType myMediaType= VIDEO TYPE; m
myFl ags = nextTimeMediaSample; / / i f this is the f i r s t frame, include the frame we are currently on i f (myFrameNum == O) myFlags I = nextTimeEdgeOK; / / i f we are maintaining the frame durations of the source movie, / / skip to the next interesting time and get the duration for that frame GetMovieNextlnterestingTime(mySrcMovie, myFlags, 1, &myMediaType, myCurMovieTime, O, &myCurMovieTime, &myDuration);
Then we need to set the current movie time of the source movie to the time just calculated and draw the movie into the offscreen graphics world: SetMovieTimeValue(mySrcMovie, myCurMovieTime) ; MoviesTask(mySrcMovie, O) ;
If the user has requested that the data rate of the destination movie be constrained, then we need to tell the standard image compression dialog component the duration of the current frame, in milliseconds. We can do that using this code:
Compressing Image Sequences 395
i f (!SCGetlnfo(myComponent, scDataRateSettingsType, &myRateSettings)) { myRateSettings.frameDuration = myDuration * 1000 / GetMovi eTi meScaI e (mySrcMovie) ; SCSetlnfo(myComponent, scDataRateSettingsType, &myRateSettings) ;
}
Finally, we can actually compress the pixel map data by calling SCCompressSequenceFrame: myErr = SCCompressSequenceFrame(myComponent, myPixMap, &myRect, &myCompressedData, &myDataSize, &mySyncFlag);
If SCCompressSequenceFrame completes successfully, then myCompressedData will hold a handle to the compressed data and myDataSize will be the size of the compressed data. In addition, the mySyncF1ag parameter will hold a value that indicates whether the compressed frame is a key frame (0) or a difference frame (mediaSampleNotSync). We will pass this value directly to the AddMediaSample function, like this: myErr = AddMediaSample(myDstMedia, myCompressedData, O, myDataSize, myDuration, (SampI eDescri pt i onHandl e)myImageDesc, 1, mySyncFlag, NULL);
And so we've completely handled a frame of the source movie.
Finishing Up Once we've exited the for loop that iterates through all the frames of the source movie, we need to do a little cleaning up. First, we need to close the compression sequence that we opened by calling SCCompressSequenceEnd: SCCompressSequenceEnd(myComponent) ; SCCompressSequenceEnd disposes of the image description and compressed data handles allocated by SCCompressSequenceBegin. Next we need to close our media editing session by calling EndMediaEdi ts: myErr = EndMediaEdi ts (myDstMedia) ;
Then we proceed as normal, inserting the edited media into the track and adding the movie resource to the destination movie file.
396
Chapter 13 Honey, I Shrunk the Kids
InsertMediaIntoTrack(myDstTrack, O, O, GetMediaDuration(myDstMedia), fixed1) ; myErr = AddMovieResource(myDstMovie, myRefNum, NULL, NULL);
And at this point, we can safely close the destination movie file: Cl oseMovieFi I e (myRefNum) ;
We also need to close down the instance of the standard image compression dialog component that's been doing all this work for us: i f (myComponent l= NULL) Cl oseComponent(myComponent) ;
Finally, let's restore the source movie to its original graphics world and movie time and dispose os the offscreen graphics world: i f (mySrcMovie ! = NULL) { / / restore the source movie's original graphics port and device SetMovieGWorld(mySrcMovie, mySavedPort, mySavedDevice); / / restore the source movie's original movie time SetMovieTimeVal ue(mySrcMovie, myOrigMovieTime) ; / / restore the original graphics port and device SetGWorld(mySavedPort, mySavedDevice) ;
/ / delete the GWorld we were drawing frames into i f (mylmageWorld ! = NULL) Di sposeGWorl d (mylmageWorl d) ;
We have now successfully compressed our image sequence. (Whew!) I won't bother to give the entire listing of the QTCmpr_CompressSequence function, as it would run for several pages. You can find the complete story in the source file OTCompress.c.
Asynchronous Compression If you run the QTCompress sample application and have it recompress a sizable movie file, you'll notice that, after you dismiss the compression settings dialog box and the file-saving dialog box, no other events are processed until the entire recompression sequence is completed. The mouse continues tracking, but you cannot access the application's menus; worse yet, the
Asynchronous Compression 397
application windows are not updated. This is all perfectly understandable, as we've jumped into a for loop and just keep processing movie frames until we are finished. One way to address this problem would be to display a progress dialog box, which we could update each time through the for loop. For good measure, we could also see if any of our windows need updating and redraw them if necessary. This would cost us some time, but it would significantly improve the user's experience. Another way to address this problem would be to compress the movie frames asynchronously. That is to say, we could launch a compression operation and then process other application events while we are waiting for the operation's completion procedure to be executed. That would allow us to redraw our windows and even grant the operating system some time to handle its own periodic tasks. QuickTime has supported asynchronous compression for many years, but until recently we had to use the lower-level services of the Image Compression Manager (namely, CompressSequenceFrame) to do so. As we've seen, there is no parameter to the SCCompressSequenceFrame function that allows us to specify a completion routine, which we would need in order to be able to use the standard image compression dialog component asynchronously. QuickTime 5, however, introduced the new function SCCompressSequenceFrameAsync, which we can use for asynchronous compression. In this section, we'll see how to modify QTCmpr_CompressSequence to support asynchronous compression.
Setting Up for Asynchronous Compression SCCompressSequenceFrameAsync is exactly like SCCompressSequenceFrame, except that it takes one additional parameter, a pointer to an ICM completion procedure record. The ICM completion procedure record, of type I CMCompletionProcRecord, is declared like this: struct ICMCompletionProcRecord { I CMCompIet i onUPP complet i onProc; long complet i onRefCon;
};
The completionProc field is a universal procedure pointer to our completion routine, and the completionRefCon field is an application-specific reference constant. When we first enter QTCmpr_CompressSequence, we'll declare and initialize some additional local variables, like this:
3198 Chapter 13 Honey, I Shrunk the Kids
# i f USE ASYNC COMPRESSION I CMComplet i onProcRecord I CMCompIet i onProcRecordPt r OSErr m
myI CMComplProcRec; myICMCompIProcPtr = NULL; myICMCompIProcErr = noErr;
myI CMComplProcRec. compl et i onProc = NULL; myI CMComplProcRec. compl et i onRefCon = OL; #endif
Then, just before we enter the for loop, we need to set some codec flags" # i f USE ASYNC COMPRESSION myFl ags = codecFl agUpdatePrevious + codecFl agUpdatePrevi ousComp + codecFl agLi veGrab; SCSetInfo (myComponent, scCodecFlagsType, &myFlags) ; #endi f n
The codecFlagUpdatePrevious and codecFlagUpdatePreviousComp flags are used to optimize temporal compression. The codecFlagLiveGrab flag is required to get SCCompressSequenceFrameAsync to work correctly (some QuickTime black magic). Once we're inside the for loop, we'll fill in the ICM completion procedure record: if
}
(myICMCompIProcPtr == NULL) { myI CMComplProcRec. compl et i onProc = NewlCMComplet i onProc (QTCmpr_Complet i onProc) ; myI CMCompI ProcRec. compIet i onRefCon = ( long) &myI CMCompI ProcErr; myI CMComplProcPt r = &myI CMComplProcRec;
For our reference constant, we're passing the address of a local variable of type OSErr. The idea is that we can undertake other processing inside the for loop as long as the value of that variable is set to a known value; the completion routine, w h e n it is triggered, will be responsible for changing that value, as we'll see shortly. Before we initiate the asynchronous compression operation, we'll set myICMComplProcErr to a default value: mylCMComplProcErr = kAsyncDefaul tVal ue;
Performing Asynchronous Compression As we've seen, we can begin an asynchronous compression operation by calling SCCompressSequenceFrameAsync, like this:
Asynchronous Compression 399
myErr = SCCompressSequenceFrameAsync(myComponent, myPixMap, &myRect, &myCompressedData, &myDataSize, &mySyncFlag, myICMCompIProcPtr) ;
The parameters here are exactly those that we earlier passed to SCCompressSequenceFrame, with the additional final parameter myICMComplProcPtr. As with all asynchronous functions, SCCompressSequenceFrameAsync returns immediately. When the compressor is finished with the compression operation, our completion routine will be executed. Listing 13.9 shows our application's completion routine. Listing 13.9 Signalingthe end of a compression operation. static PASCAL_RTNvoid QTCmpr_CompletionProc (OSErr theResult, short theFlags, long theRefCon)
{
OSErr
*myErrPtr = NULL;
i f (theFlags & codecCompletionDest) { myErrPtr = (OSErr *)theRefCon; i f (myErrPtr != NULL) *myErrPtr = theResult;
}
When the codecCompletionDest flag is set in the theFlags parameter, we know that the compression operation has finished successfully. At that point, we can copy the result code passed to our completion routine into the location specified by theRefCon. This effectively changes the value of the myICMComplProcErr variable in our for loop, where we are executing this while loop until that value changes: while (myICMCompIProcErr == kAsyncDefaultValue) { EventRecord myEvent; WaitNextEvent(O, &myEvent, 60, NULL); SCAsyncI dl e (myComponent) ;
As you can see, we're repeatedly calling WaitNextEvent to yield time to other processes and SCAsyncIdl e to yield time to the compressor component. I'll leave it as another reader exercise to add the code necessary to handle application events (for example, update events).
400
Chapter 13 Honey, I Shrunk the Kids
Weighing the Benefits While adding support for asynchronous compression is not too terribly complicated, you might be wondering whether it's worth the trouble. In other words, are we going to see appreciable performance gains, or at least significant user experience improvements? Sadly, the answer is no, at least at the moment. First of all, not all compression components support asynchronous operation. (Indeed, at this point, there is only one Apple-supplied codec capable of performing asynchronous compression, the H.263 codec.) You can still invoke compression components using SCCompressSequenceFrameAsync, but SCCompressSequenceFrameAsync will not return to the caller until the compression is complete and the completion routine has been executed. In other words, it will run synchronously. Second, and perhaps more important, even codecs that are able to compress asynchronously will not show any real improvement when running on a single-processor machine.
Conclusion The standard image compression dialog component is a prime example of the kind of QuickTime APIs that I like best: it provides a high-level toolbox for handling a wide variety of typical image compression tasks, allowing us to compress both individual images and sequences of images. Moreover, it gives us a standard user interface for eliciting compression settings from the user (rather in the same way that the Standard File Package or the Navigation Services provides a standard user interface for eliciting files from the user).
Conclusion 4.01
This Page Intentionally Left Blank
A Goofy Movie
Introduction The major new technology introduced in QuickTime version 2.1 (released in 1995) was support for sprites and sprite animation. A sprite is a graphical object that has a number of properties, including its current image, location, size, layer, graphics mode, and visibility state. These properties determine the appearance of the sprite at any instant in time. By varying one or more of these properties over time, we can animate the sprite. For instance, Figure 14.1 shows the first frame of a QuickTime movie that contains one sprite, whose image is the original icon for the QuickTime system extension. We can move the icon to the right by gradually changing the horizontal location of the sprite. In addition, we can change the image associated with the sprite at any time. In this case, we'll change the icon from the old QuickTime extension
Figure 14.1 A movie with a sprite.
403
icon to the new QuickTime extension icon w h e n the sprite gets to the halfway point, as shown in Figure 14.2. The new icon then continues moving at the same rate until it reaches the right side of the movie box. Figure 14.3 shows another sprite movie. There is only one image associated with the sprite for the entire duration of the movie, and the sprite location remains constant. We'll animate this sprite by changing the graphics mode, from totally transparent to totally opaque. What we've done here is re-create, using sprite animation, the appearing-penguin movie that was the very first QuickTime movie we built in this book.
Figure 14.2 The sprite with a new image.
Figure 14.3 The penguin movie using sprite animation.
404
Chapter 14 A Goofy Movie
Recall how we went about creating the first version of our penguin movie: we opened a picture resource and drew that picture into an offscreen graphics world; we compressed the data in that graphics world and added it as a frame to the movie file. Then we repeated the drawing and compression 99 times, each time with gradually more opacity, to create 99 additional movie frames. The resulting movie file contained 100 compressed images, for a total size of about 472 Kbytes. Using sprite animation, we can reduce that size dramatically. Our new penguin movie file contains only a single compressed image and 100 sets of "instructions" that indicate the desired level of opacity in each movie frame. The 100 movie frames are generated at playback time by the sprite media handler from that image and those 100 sets of instructions. The size of the sprite version of the penguin movie is only about 36 Kbytes. (No, that's no typo; the exact same visual output can be achieved with a file less than 1/10th the size of our original movie file.) Already you can see that sprite animation differs significantly from what's often called eel animation, where each frame os the animation is a fully rendered picture of some characters superimposed on a background image. If the instructions in a sprite movie aren't too complicated, they can be executed at runtime just as smoothly as decompressing and playing back the fully rendered version of the movie. And, as we've seen, the images and instructions can take up a lot less space. QuickTime 3.0 extended the capabilities of the sprite media handler by adding support for wired sprites, or sprites that react to mouse events (and other kinds of events) and that have various actions attached (or "wired") to them. For instance, we can use a wired sprite to control the properties of the sprites in the movie (so that clicking one sprite causes another sprite to disappear or to change location). Or, we can control various aspects of movie playback, such as the volume and balance of a sound track or the graphics mode of a video track. By using wired sprites in a movie, we can add a level of interactivity previously unavailable in QuickTime. In this chapter, we're going to learn how to create movies that contain sprite tracks. We'll see how to create both the icon movie shown in Figure 14.1 and the penguin movie shown in Figure 14.3. Our sample application for this chapter is called QTSprites, and its Test menu is shown in Figure 14.4. (The "space movie" is a more complex movie that does sprite animation using location changes, layer changes, and image changes. We won't learn how to build it in this chapter, but the code for doing so is contained in the file QTSprites. c.) We'll begin by looking in more detail at sprites and their properties. Then we'll take a look at the structure of sprite tracks and see how to build the icon and penguin sprite movies. Toward the end of the chapter, we'll see how to add some simple interactivity to our sprite movies without using
Introduction 405
Figure 14.4
The Test menu of QTSprites.
wired sprites. We'll postpone our investigation of wired sprites to Chapter 16, "Wired."
Sprite Properties In a QuickTime movie, a sprite is a graphical object that belongs to a sprite track (of type Spri teMedi aType). In the simplest case, the basic appearance of a sprite is set by selecting one out of an array of images associated with the sprite. The current sprite image index is one of the five main sprite properties, defined using these constants: enum { kSpri tePropertyMat ri x kSpri tePropertyV i s i bl e kSpri tePropertyLayer kSpri tePropertyGraphi csMode kSpri tePropertyImageIndex
=1, = 4,
= 5~ = 6~ =
100
The sprite matrix is a 3-by-3 matrix that controls the location, size, and rotation of the sprite image within the sprite track. (A sprite's matrix is added to the track matrix of the sprite track.} The sprite visibility state is an integer value that controls whether the sprite is currently visible. {This value is interpreted as a Boolean value but is stored in the movie file as a 16-bit short integer.} The sprite layer is an integer value that determines, w h e n two or more sprites have locations that overlap, which sprite is drawn on top of the other sprite{s). Sprites with lower layer values are drawn on top of sprites with higher layer values; if we want to ensure that some sprite is drawn behind all other overlapping sprites, we can set its layer to the special value kBackgroundSpri teLayerNum (appropriately defined in the file Movies.ha standard header file provided by QuickTime--as 32767). Finally, the sprite graphics mode determines how the sprite is drawn into the sprite track. We specify a sprite graphics mode using a structure of type ModifierTrackGraphi csModeRecord, defined like this:
40G
Chapter 14 A Goofy Movie
struct ModifierTrackGraphicsModeRecord { 1ong graphi csMode; RGBColor opColor;
}~
This structure contains the QuickDraw graphics mode and the color used by some of those graphics modes. For instance, if graphicsMode is blend, then opCo] or specifies the weight color {which determines the amount of blending of the source and destination pixels). A sprite's current image does not have to come from a sprite image array. Instead, the sprite media handler can use a video track in the same QuickTime movie as the source for the sprite's image. This allows us to create even more intricate animations than are possible by simply varying the five basic sprite properties. As a very simple example, we could create a sprite track containing two sprites, one whose image looks like a television set and a superimposed sprite whose images are derived from a video track. The net effect would be a sprite track containing a television playing the video.
(~ Sprite Tracks A sprite track consists of one or more sprite media samples. There are two basic kinds of sprite media samples: those that define the sprite image array and set the initial properties of a frame, and those that animate the sprites in the track by specifying changes to the sprites' properties. The sprite media handler relies on the distinction between key frames and difference frames, which we encountered in the previous chapter. The image arrays and initial properties are stored in key frames, and the sprite property changes are stored in difference frames. The only departure here is the terminology: when we're working with sprite data, the difference frames are called override frames {because the data in those frames overrides the data in the key frames}.
The Format of Key Frame Samples The data in both key frames and override frames is stored in atom containers. A key frame atom container contains a child atom of type kSpri teAtomType for each sprite in the key frame. This atom contains leaf atoms that define the initial properties of the sprite. The atom IDs of the sprite atoms are numbered sequentially, starting at 1; these atom IDs are also called sprite IDs. Figure 14.5 shows the basic structure of a key frame sample. A sprite atom contains child atoms that define the initial properties of the sprite. It can contain a child for each of the five basic sprite properties, as
Sprite Tracks 407
I Atom container I I
I kSpriteAtomType
kSpriteSharedDataAtomType 1
I
Sprite property atoms
I kSpriteAtomType n
Sprite shared data atoms
I
Sprite property atoms
Figure 14.5 The structure of a key frame sample.
kSpriteAtomType n
I kSpritePropertylmagelndex 1 Short integer
I kSpritePropertyLayer 1
Short integer
t kSpritePropertyGraphicsMode 1
ModifierTrackGraphicsModeRecord
I kSpritePropertyMatrix 1 MatrixRecord
I kSpritePropertyVi si ble 1 Short integer
I kSpriteNameAtomType 1 String of characters
Figure 14.6 The structure of a sprite atom.
well asanatomof typekSpriteNameAtomType thatdefinesthe sprite'sname. Figure 14.6 shows the structure of a sprite atom.
408
Chapter 14 A Goofy Movie
kSpriteSharedDataAtomType I
kSpritelmagesContainerAtomType I
kSpritelmageAtomType 1
I
I
kSpriteImageDataAtomType 1
Image data
kSpritelmageAtomType
]
n
I
kSpritelmageDataAtomTypel
I
Image data
Figure 14.7 The structure of the shared sprite data.
As Figure 14.5 indicates, a key frame atom container also contains a single child atom of type kSpriteSharedOataAtomType (with atom ID 1), which contains the image data for all the sprites. This atom contains one sprite images container atom, of type kSpriteImagesContainerAtomType (also with atom ID 1). This atom, in turn, contains one atom of type kSpriteImageAtomType for each individual image in the key frame. Note that all the images for all the sprites are contained in the single images container atom. Figure 14.7 shows the structure of a sprite shared data atom.
The Format of Override Samples The structure of an override sample is somewhat simpler than that of a key frame sample, largely because an override sample does not contain any image data. An override sample is an atom container that contains a sprite atom (of type kSpri teAtomType) for each sprite that is being animated by that override sample. The sprite atoms contain child atoms for each of the properties that are changing from the previous key frame or override sample. Figure 14.8 shows the structure of a typical override sample. The ID of a sprite atom in the override sample should be the same as the ID of the sprite atom in the key frame atom whose data the override atom is overriding.
0
Creating Sprite Tracks As we've just learned, a sprite track consists of key frame samples that contain the images for the sprites in a track and the initial properties of those sprites, and override samples that change one or more of the properties of
Creating Sprite Tracks 4 0 9
I Atom container I
I kSpriteAtomType m
Sprite property atoms
I kSpriteAtomType n Sprite property atoms
Figure 14.8 The structure of an override sample. those sprites. In both cases, the sample data is contained in an atom container. Building a sprite track is, therefore, largely a matter of creating the appropriate atom containers and inserting them as media samples at the desired times in the sprite track media.
Creating Sprite Tracks and Media When the user selects an item in the Test menu, QTSprites calls the QTApp_HandleMenu function, which is shown in Listing 14.1.
Listing 14.1 Handling items in the Test menu. Boolean QTApp_HandleMenu (UInt16 theMenultem)
{
Boolean
mylsHandled = false;
switch (theMenultem) { case IDM MAKEICONSMOVIE: case IDM MAKEPENGUINMOVIEcase IDM MAKESPACEMOVIE. QTSprites_CreateSpritesMovie(theMenuItem) ; mylsHandled = true; break; case IDM USE BACKGROUNDIMAGE. gUseBackgroundPicture = !gUseBackgroundPicture; mylsHandled = true; break; D
m
return (mylsHandled) ;
410
Chapter 14 A Goofy Movie
As you can see, we call the function QTSprites_CreateSpritesMovie to create each of the three sample movies, passing in the m enu item so that we
know which movie to create. The function QTSprites_CreateSpritesMovie is defined in Listing 14.2.
Listing 14.2. Creating a sprite movie. OSErr QTSprites_CreateSpritesMovie (UInt16 theMenuItem) Movie Track Medi a FSSpec Boolean Boolean Fixed Fixed StringPtr StringPtr long short short OSErr
myMovie = NULL; myTrack = NULL; myMedia = NULL; myFile; mylsSel ected = false; myIsRepl acing = false; myHeight = O; myWidth = O; myPrompt = QTUti I s ConvertCToPascalString(kSpri teSavePrompt) ; myFi leName = QTUti I s ConvertCToPascal String(kSpri teSaveMovieFi leName) ; myFlags = createMovieFileDeleteCurFi le I createMovieFileDontCreateResFile; myResRefNum = O; myResID = movieInDataForkResID; myErr = noErr;
/ / prompt the user for the destination filename QTFrame_PutFile(myPrompt, myFileName, &myFile, &mylsSelected, &mylsReplacing) ; myErr = mylsSelected ? noErr : userCanceledErr; i f (!mylsSelected) goto bai I ; / / create a movie f i l e for the destination movie myErr = CreateMovieFile(&myFile, FOURCHAR CODE('TVOD'), smSystemScript, myFlags, &myResRefNum, &myMovie); i f (myErr ! = noErr) goto bai I ; / / create the sprite track and media QTSprites GetMovieSize(theMenuItem, &myHeight, &myWidth); myTrack = NewMovieTrack(myMovie, myWidth, myHeight, kNoVolume); myMedia = NewTrackMedia(myTrack, SpriteMediaType, kSpriteMediaTimeScale, NULL, 0); Begi nMediaEdi ts (myMedia) ;
Creating Sprite Tracks 411
/ / add the appropriate samples to the sprite media switch (theMenultem) { case IDM MAKE ICONS MOVIE: QTSpri tes AddlconMovieSamplesToMedia (myMedia) ; break; case IDM MAKE PENGUIN MOVIE. QTSpri tes_AddPengui nMovieSamplesToMedia (myMedia) ; break; case IDM MAKE SPACE MOVIEQTSpri tes_AddSpaceMovieSamplesToMedia (myMedi a) ; break; default: goto bai I ; EndMediaEdi ts (myMedia) ; / / add the media to the track InsertMedialntoTrack(myTrack, O, O, GetMediaDuration(myMedia), f i x e d l ) ; / / set the sprite track properties QTSpri tes_SetTrackProperties (myMedia, theMenultem) ; / / add the movie resource to the movie f i l e myErr = AddMovieResource(myMovie, myResRefNum, &myResID, myFile.name); bail. i f (myResRefNum ! = O) Cl oseMovi eFi I e (myResRefNum) ; i f (myMovie ! = NULL) Di sposeMovie (myMovie) ; free (myPrompt) ; free (myFi I eName) ; return (myErr) ;
The QTSprites_CreateSpritesMovie function is remarkably similar to each of the other movie creation functions we've used earlier in this book. There are only three main additions for QTSprites. First, since we want to be able to create any one of three different sprite movies, we call the function QTSprites_fietMovieSize to get the desired size for each of those movies. QTSpri tes_GetMovieSize is defined in Listing 14.3.
412
Chapter 14A GoofyMovie
Listing 14.3 Getting the size of a sprite movie. void QTSprites GetMovieSize (UInt16 theMenultem, Fixed *theHeight, Fixed *theWidth)
{
if
((theHeight == NULL) II (theWidth == NULL)) return;
switch (theMenultem) { case IDM MAKE ICONS MOVIE: *theWidth = (long)klconSpriteTrackWidth << 16; *theHeight = (long)klconSpriteTrackHeight << 16; break; case IDM MAKE PENGUIN MOVIE" *theWidth = (long)kPenguinSpriteTrackWidth << 16; *theHeight = (long)kPenguinSpriteTrackHeight << 16; break; case IDM MAKE SPACE MOVIE*theWidth = (long)kSpaceSpriteTrackWidth << 16; *theHeight = (long)kSpaceSpriteTrackHeight << 16; break;
} } This is pretty simple stuff; we just convert some long integer constants to the Fi xed data type and return them to the caller. The second difference between QTSprites_CreateSpritesMovie and our earlier movie creation functions is that we use the menu item number passed in to select the appropriate function for adding samples to the sprite media. And, third, once we've added those samples to the media, we call the QTSprites_SetTrackProperties function to set some sprite track properties. We'll consider sprite track properties in more detail next.
Setting Sprite Properties A key frame sample is an atom container that contains an atom of type kSpri teAtomType for each sprite in the frame [which contains the initial properties of the sprite) and an atom of type kSpriteSharedBataAtomType {which contains atoms that hold the sprite images). So the first thing we need to do is create an atom container, like this: myErr = QTNewAtomContainer(&mySampl e) ;
Let's begin by adding the sprite atoms to the key frame sample. A sprite atom is itself an atom container because it contains child atoms for each of
Creating Sprite Tracks 4 1 3
the initial sprite properties we want to assign it. (Any properties we don't explicitly define in a key frame sample are set to default values.) So we need to create another atom container, like so: myErr = QTNewAtomContainer(&mySpri teData) ;
Now we want to add one or more property atoms to the sprite atom. For the icon movie, we want to set the initial location, visibility state, layer, and image index. We can set the image index, for instance, like this: short
mylndex = 1;
mylndex = EndianS16 NtoB(myIndex) ; myErr = QTInsertChild(mySpriteData, kParentAtomlsContainer, kSpritePropertyImageIndex, i , O, s i z e o f ( s h o r t ) , &myIndex, NULL);
This code inserts a child of type kSpritePropertyImageIndex into the sprite atom, making sure that the atom data (in this case, a short integer whose value is 1) is in big-endian format. Similarly, we can set the initial visibility state of the icon sprite using these lines of code: short isVisible = true; isVisible = EndianS16 NtoB(isVisible); myErr = QTInsertChild(mySpriteData, kParentAtomlsContainer, kSpritePropertyVisible, 1, O, s i z e o f ( s h o r t ) , &isVisible, NULL);
(You might have t h o u g h t that the default value for the visibility state of a sprite would be true, but sadly that's not so. So we need to explicitly configure our sprites to be visible or they w o n ' t be drawn.) To increase the readability of our code, we'll define a utility function called SpriteUtils_SetSpriteData that allows us to set the main sprite properties in one fell swoop. Then we can define the initial state of the icon sprite like this: myLocation.h = 32; myLocati on. v = 32; isVisible = true; myLayer -- -1; mylndex = I; SpriteUtils SetSpriteData(mySpriteData, &myLocation, &isVisible, &myLayer, &mylndex, NULL, NULL, NULL); u
414
Chapter 14 A Goofy Movie
The Spri teUti I s_SetSpri teData function is defined in the file Spri teUti I i ties.c; its definition is shown in Listing 14.4.
Listing 14.4 Setting properties of a sprite. OSErr SpriteUtils_SetSpriteData ( QTAtomContai ner theSprite, Point *theLocation, short *theVisible, short *theLayer, short *thelmagelndex, Modi fi erTrackGraphi csModeRecord *theGraphi csMode, StringPtr theSpri teName, QTAtomContai ner theAct i onAtoms) QTAtom OSErr
myPropertyAtom; myErr = noErr;
/ / set the sprite location data i f (theLocation l= NULL) { Matri xRecord myMatri x; Set Identi tyMatri x (&myMatri x) ; myMatrix.matrix[2] [0] = ((long)theLocation->h << 16) ; myMatrix.matrix[2][1] = ((long)theLocation->v << 16); EndianUtils MatrixRecord NtoB(&myMatrix); myPropertyAtom = QTFindChildBylndex(theSprite, kParentAtomlsContainer, kSpritePropertyMatrix, 1, NULL); i f (myPropertyAtom == O) myErr - QTInsertChild(theSprite, kParentAtomI sContai ner, kSpritePropertyMatrix, 1, O, sizeof(MatrixRecord), &myMatrix, NULL); else myErr = QTSetAtomData(theSprite, myPropertyAtom, sizeof(MatrixRecord), &myMatrix) ; i f (myErr I= noErr) goto bai I ; / / set the sprite v i s i b i l i t y state i f (theVisible l: NULL) { short myVisible = *theVisible;
Creating Sprite Tracks 4'115
myVisible = EndianS16_NtoB(myVisible) ; myPropertyAtom = QTFindChiIdByIndex(theSprite, kParentAtoml sContai ner, kSpritePropertyVisible, 1, NULL); i f (myPropertyAtom == O) myErr = QTInsertChild(theSprite, kParentAtoml sContai ner, kSpritePropertyVisible, 1, O, sizeof(short), &myVisible, NULL); else myErr = QTSetAtomData(theSprite, myPropertyAtom, sizeof(short), &myVisible) ; i f (myErr ! = noErr) goto bai I ; / / set the sprite layer i f (theLayer I= NULL) ( short myLayer = *theLayer; myLayer = EndianS16 NtoB(myLayer) ; myPropertyAtom = QTFindChi I dBylndex (theSpri te, kParentAtoml sContai ner, kSpritePropertyLayer, 1, NULL); i f (myPropertyAtom == O) myErr = QTInsertChild(theSprite, kParentAtoml sContai ner, kSpritePropertyLayer, I, O, sizeof(short), &myLayer, NULL); else myErr = QTSetAtomData(theSprite, myPropertyAtom, sizeof(short), &myLayer) ; i f (myErr I= noErr) goto bai I ; / / set the sprite image index i f (thelmagelndex ! = NULL) { short mylmagelndex = *thelmagelndex; mylmagelndex = EndianS16 NtoB(myImagelndex);
416
Chapter 14 A Goofy Movie
myPropertyAtom = QTFindChildBylndex(theSprite, kParentAtoml sContai ner, kSpritePropertylmagelndex, 1, NULL); i f (myPropertyAtom == O) myErr = QTInsertChild(theSprite, kParentAtomlsContai ner, kSpritePropertylmageIndex, 1, O, sizeof(short), &mylmagelndex, NULL); else myErr = QTSetAtomData(theSprite, myPropertyAtom, sizeof(short), &myImagelndex) ; i f (myErr I= noErr) goto bai I ; / / set the sprite graphics mode i f (theGraphicsMode l= NULL) { Modi f i erTrackGraphi csModeRecord
myGraphicsMode;
myGraphi csMode,graph i csMode = EndianU32_NtoB(theGraph i csMode->graphi csMode) ; myGraphicsMode.opColor.red = EndianU16 NtoB(theGraphicsMode->opColor.red) ; myGraphicsMode.opColor.green = EndianU16 NtoB(theGraphicsMode->opColor.green) ; myGraphicsMode.opColor.blue = EndianU16 NtoB(theGraphicsMode->opColor.blue) ; m
myPropertyAtom = QTFindChi I dBylndex (theSpri te, kParentAtoml sContai ner, kSpritePropertyGraphicsMode, I, NULL); i f (myPropertyAtom == O) myErr = QTInsertChild(theSprite, kParentAtoml sContai ner, kSpritePropertyGraphicsMode, 1, O, si zeof (myGraphi csMode), &myGraphicsMode, NULL); else myErr = QTSetAtomData(theSprite, myPropertyAtom, sizeof(myGraphicsMode), &myGraphicsMode) ; i f (myErr i= noErr) goto bai I ; / / set the sprite name i f (theSpriteName !: NULL) { QTAtom mySpri teNameAtom;
Creating Sprite Tracks 417
mySpri teNameAtom = QTFi ndChi I dByIndex (theSpri te, kParentAtoml sContai ner, kSpriteNameAtomType, 1, NULL); i f (mySpriteNameAtom == O) myErr = QTInsertChild(theSprite, kParentAtomI sContai ner, kSpriteNameAtomType, 1, O, theSpriteName[O] + 1, theSpriteName, NULL); else myErr = QTSetAtomData(theSprite, mySpriteNameAtom, theSpriteName[O] + I, theSpriteName); if
(myErr l= noErr) goto bai I ;
/ / set the action atoms i f (theActionAtoms ! = NULL) myErr = QTInsertChildren(theSprite, kParentAtomIsContainer, theActionAtoms) ; bail 9 i f ((myErr l= noErr) && (theSprite l= NULL)) QTRemoveChildren(theSprite, O) ; return (myErr) ;
For each parameter that is not NULL, the function SpriteUti 1s_SetSpriteData looks to see whether the sprite atom container already contains an atom of the corresponding type (by calling QTFindChildByIndex). If it does contain such an atom, then SpriteUtils SetSpriteData calls QTSetAtomData to reset the data in that atom; otherwise, it calls QTInsertChi l d to add an atom of that type. In all cases, the data passed in is converted to bigoendian format before being inserted into an atom. Now that the mySpri teData atom container holds a child atom for each initial property we want to set, we need to add it to the key frame sample atom container, mySample. Once again, we'll define a utility function to help us out: Spri teUt i I s_AddSpri teToSampl e (mySample, mySpriteData, kQTIconSpri teAtomID) ;
Spri teUti 1s_AddSpri teToSampl e is defined in Listing 14.5.
418 Chapter14 A Goofy Movie
Listing 14.5 Adding a sprite data atom to a sample container. OSErr Spri teUti I s_AddSpri teToSample (QTAtomContainer theSample, QTAtomContainer theSprite, QTAtomID theSpriteID)
(
QTAtom OSErr
mySpriteAtom = O; myErr = paramErr;
/ / see i f the sample already contains a sprite atom of the specified ID mySpriteAtom = QTFindChildByID(theSample, kParentAtoml sContai ner, kSpriteAtomType, theSpriteID, NULL); i f (mySpriteAtom != O) goto bai I ; / / here, the index 0 means to append the sprite to the sample myErr = QTInsertChild(theSample, kParentAtomlsContainer, kSpriteAtomType, theSpriteID, O, O, NULL, &mySpri teAtom) ; i f (myErr ! = noErr) goto bai I ; myErr = QTInsertChildren(theSample, mySpriteAtom, theSprite); bail: return (myErr) ;
}
As you can see, SpriteUtils_AddSpriteToSample first calls QTFindChildByID
to determine whether the specified atom container already contains a sprite atom with the specified sprite ID. If there is an atom of that ID already in the sample, SpriteUtils_AddSpriteToSample returns an error to the caller. Otherwise, it calls QTInsertChild to create a sprite atom of that ID in the sample atom container. Then it calls QTInsertChi ]dren to copy the children from the sprite atom container passed as a parameter into the newly inserted atom in the sample.
Setting Sprite Images So far, then, we've created a sprite atom that contains the desired initial properties of the icon sprite and added it to the key frame atom container mySample. Now we need to create an atom of type kSpri teSharedDataAtomType
Creating Sprite Tracks 4 1 9
and add the required subatoms to it that contain the sprite image data. In the case of the icon sprite movie, we need to add two images to that atom, one for the old QuickTime extension icon and one for the new icon. The function QTSpri tes_AddIconMovi eSampl esToMedi a does this by executing these lines of code: myKeyColor.red = myKeyColor.green = myKeyColor.blue = Oxffff; Spri teUt i I s AddPI CTImageToKeyFrameSample (mySample, kOldQTIconID, &myKeyColor, 1, NULL, NULL); Spri teUt i I s_AddPI CTImageToKeyFrameSample (mySample, kNewQTIconID, &myKeyColor, 2, NULL, NULL);
Once again, we're relying on a function defined in the file S p r i t e U t i l i t i es. c to hide the nitty-gritty details of building the required atom from our main application. Spri teUti 1s_AddPICTImageToKeyFrameSampl e (defined in Listing 14.6) reads a picture from our application's resource fork, recompresses the picture with the specified color as a transparency color, and then adds it to the key frame sample atom container.
Listing 14.6 Adding compressed image data to a key frame sample. OSErr Spri teUti I s AddPICTImageToKeyFrameSample (QTAtomContainer theKeySample, short thePictID, RGBColor *theKeyColor, QTAtomID theID, FixedPoint *theRegistrationPoint, StringPtr thelmageName) m
PicHandle Handle ImageDescri pti onHandle OSErr
myPicture = NULL; myCompressedPicture = NULL; mylmageDesc = NULL; myErr = noErr;
/ / get picture from resource myPicture = (PicHandle)GetPicture(thePictID) ; i f (myPicture == NULL) myErr = resNotFound; i f (myErr l= noErr) goto bai I ; DetachResource ( (Handl e)myPi cture) ; / / convert i t to image data compressed by the animation compressor myErr = ICUti I s_RecompressPictureWi thTransparency (myPicture, theKeyColor, NULL, &mylmageDesc, &myCompressedPicture); i f (myErr != noErr) goto bai I ;
420
Chapter 14 A Goofy Movie
/ / add i t to the key sample HLock(myCompressedPicture) ; myErr = Spri teUti I s_AddCompressedlmageToKeyFrameSample (theKeySample, mylmageDesc, GetHandleSize(myCompressedPicture), *myCompressedPicture, theID, theRegistrationPoint, thelmageName); bail: i f (myPicture ! = NULL) Ki I l Picture (myPicture) ; i f (myCompressedPicture != NULL) Di sposeHandle (myCompressedPicture) ; i f (mylmageDesc ! = NULL) Di sposeHandle ( (Handle)mylmageDesc) ; return (myErr) ;
SpriteUtils AddPICTImageToKeyFrameSampledoes most of its work by calling twvo other utility functions, ICUtil s_RecompressPictureWithTransparency and SpriteUtils_AddCompressedlmageToKeyFrameSample. We won't dissect either of these functions in detail here, as that would take us too far afield. Sprite-
Utils_AddCompressedImageToKeyFrameSample really just does what you'd imagine to build the atom container illustrated in Figure 14.7.
Adding the Key Frame Sample to the Sprite Media So, we've managed to build the key frame sample. Now we just need to add it to the sprite media. To do this, we first need to create a handle to a sprite sample description, which we'll pass to AddMediaSample. A sprite sample description is defined by the Spri teDescri ption data type, declared like this: struct SpriteDescription long long long short short long OSType long
};
{ descSize; dataFormat; resvdl; resvd2; dataReflndex; version; decompressorType; sampleFl ags;
Creating Sprite Tracks 421
We allocate a handle to a sprite sample description by calling NewHandleClear: mySampleDesc = (SampleDescri pt i onHandle) NewHandleCl ear (si zeof (Spri teDescri pti on) ) ;
The decompressorType field of the sprite sample description specifies the type of compressor used to compress the sample data (or 0 if no compression is used). The sprite media handler supports compressed sample data; the only restriction is that the compressor used to compress the data must be lossless. (A compressor is lossless if the result of compressing some data and then decompressing it yields the original data unchanged; otherwise, the compressor is lossy.) For the moment, we'll just use the uncompressed data that we've created. So we can just go right ahead and call AddMediaSample: myErr = AddMediaSample(theMedia, (Handle)mySample, O, GetHandleSize(mySample), kSpriteMediaFrameDurationIcon, mySampleDesc, I, O, NULL);
We can use yet another utility function defined in S p r i t e U t i l i t i e s . c " Spri teUt i I s_AddSpri teSampI eToMedia (theMedi a, mySampI e, kSpriteMediaFrameDurationlcon, true, NULL); Listing 14.7 shows our definition of Spri teUti I s_AddSpri teSampl eToMedi a.
Listing 14.7 Adding a sprite sample to the sprite media. OSErr SpriteUtils AddSpriteSampleToMedia (Media theMedia, QTAtomContainer theSample, TimeValue theDuration, Boolean isKeyFrame, TimeValue *theSampleTime) E
{
SampleDescri pti onHandl e OSErr
mySampl eDesc = NULL; myErr = noErr;
mySampleDesc : (SampleDescriptionHandle)NewHandleClear(sizeof(SpriteDescription)) ; i f (mySampleDesc =-- NULL) { myErr = MemError() ; goto bai I ;
}
422
Chapter 14 A Goofy Movie
myErr = AddMediaSample (theMedi a, (Handl e) theSampl e, O, GetHandl eSi ze (theSampl e), theDuration, mySampleDesc, 1, (short) (i sKeyFrame ? 0 : mediaSampleNotSync), theSampl eTime) ; bail: i f (mySampleDesc ! = NULL) DisposeHandle((Handle) mySampleDesc); return (myErr) ;
}
Note that SpriteUtils_AddSpriteSampleToMedia adds the specified sample data as a key frame sample or an override frame sample, depending on the value of the i sKeyFrame parameter.
Creating Override Samples With these sprite utility functions at our disposal, it's now quite easy to create some override samples and add them to the sprite media. For the icon movie, we want to create 99 override samples. Each override sample will move the icon 2 pixels to the right; furthermore, w h e n the icon reaches the halfway point, we'll change the image index from 1 to 2. Listing 14.8 shows the code we use to add those override samples to the sprite media.
Listing 14.8 Adding some override samples. for (myCount = I; myCount <= kNumOverrideSamples; myCount++) { QTRemoveChiI dren(mySample, kParentAtomIsContainer) ; QTRemoveChiI dren(mySpri teData, kParentAtomlsContainer) ; / / every frame, bump the icon's location myLocation.h += 2; / / change icon halfway through i f (myCount : : kNumOverrideSamples / 2) myl ndex = 2;
Creating Sprite Tracks 423
Spri teUti I s_SetSpri teData (mySpri teData, &myLocation, NULL, NULL, &mylndex, NULL, NULL, NULL); Spri teUti I s_AddSpri teToSample (mySample, mySpriteData, kQTIconSpri teAtoml D) ; SpriteUti I s_AddSpriteSampleToMedia(theMedia, mySample, kSpriteMediaFrameDurationlcon, false, NULL);
Setting Sprite Track Properties We saw earlier in this chapter that we can use a sprite image as the background of a sprite track by setting the layer of the image to kBackgroundSpriteLayerNum. But what if we want to have a solid background color for the entire sprite track? We could of course create a sprite image of the proper size and color and then put it into the first key frame sample of the sprite track. The sprite media handler, however, provides a better method to set a solid background color by allowing us to set the sprite track's background color property. The background color property is one of several sprite track properties that control global aspects of the sprite track. The currently defined sprite track properties are accessed using these constants: enum { kSpri teTrackPropertyBackgroundCol or kSpri teTrackPropertyOffscreenBi tDepth kSpri teTrackPropertySampl eFormat kSpri teTrac kPropertyScal eSpri tesToScaI eWorl d kSpri teTrac kPropertyHasAct i ons kSpri teTrackPropertyV i si bl e kSpri teTrac kPropertyQTIdl eEventsFrequency
};
= 101, = 102, = 103, = 104, = 105, = 106, = 107
To set one or more sprite track properties, we need to create a media property atom, an atom container that contains a child atom for each of the properties we want to set. The atom type of the child atom should be set to one of these sprite property constants, and the atom ID should be set to 1. The type of the atom data varies from property to property. For the background color sprite track property, the atom data is an RGBColor structure. Once we've constructed a media property atom, we attach it to the sprite track by calling the SetMediaPropertyAtom function. Listing 14.9 shows how we set a solid white background for the penguin sprite movie. (If we don't specify a background sprite image and we don't set the background color property, then we'll get the default sprite track background color, which is black.)
424
Chapter 14 A Goofy Movie
Listing 14.9 Setting the background color of a sprite track. void QTSprites SetTrackProperties (Media theMedia, UInt16 theMenultem)
{
m
QTAtomContainer RGBColor OSErr
myTrackPropert i es; myBackgroundCol or; myErr = noErr;
i f (!gUseBackgroundPicture) { / / add a background color to the sprite track QTSprites_GetBackgroundColor(theMenultem, &myBackgroundColor) ; myErr = QTNewAtomContainer(&myTrackProperties) ; i f (myErr == noErr) { QTInsertChild(myTrackProperties, O, kSpriteTrackPropertyBackgroundColor, 1, 1, sizeof(RGBColor), &myBackgroundColor, NULL); SetMediaPropertyAtom(theMedia, myTrackProperti es) ; QTDisposeAtomContai ner (myTrackPropert i es) ;
} }
The kSpriteTrackPropertyOffscreenBi tDepth property specifies the desired bit depth of the offscreen graphics world where the sprite data is drawn before it is copied to the screen. The atom data is of type short, and the default value is 0 (which means to use the bit depth of the deepest monitor that intersects the onscreen sprite window). Setting this property can save memory if your sprite graphics are drawn at a lower bit depth than the user's monitor. The kSpriteTrackPropertySampleFormat property specifies the override sample interpretation mode, which indicates how the sprite media handler interprets override samples. Currently there are two possible modes: enum { kKeyFrameAndSi ngl eOverri de kKeyFrameAndA110verri des
};
= 1L<<
1,
= 1L<<2
If the override sample interpretation mode is kKeyFrameAndSingl eOverride, then the sprite media handler generates the sprite data for a particular override sample by applying the changes in that override sample directly to the key frame sample, ignoring any previous override samples. (This is the default mode.) On the other hand, if the mode is kKeyFrameAndAll0verrides,
Creating Sprite Tracks 4 2 5
then the sprite media handler generates sprite data by applying the changes in a particular override sample to the data generated by applying the changes in all previous override samples to the key frame sample data. In other words, kKeyFrameAndSing] eOverri de specifies that a particular override frame contains absolute changes to key frame data, while kKeyFrameAndA]]Overrides specifies relative changes. The kSpri teTrackPropertyScal eSpri tesToScaleWorld property specifies whether the sprites in the sprite track are rescaled whenever the sprite track is resized. This is useful mostly when the sprite images have been compressed using a resolution-independent codec {for example, the Curve codec). The atom data is of type Boolean, and the default value is fa] se. The kSpriteTrackPropertyVisible property specifies whether the sprite track is visible. The atom data is of type Boolean, and the default value is true. It might occasionally be useful to set the sprite track to be invisible if there are other tracks in the movie and you want to allow the user to click items in those tracks {by putting an invisible sprite track in front that intercepts those clicks}. The remaining two sprite track properties apply only to sprite tracks that contain wired actions. The kSpriteTrackPropertyHasActions property specifies whether the sprite track contains any wired actions; the atom data is of type Boolean and the default value is false. The kSpriteTrackPropertyOTIdleEventsFrequency property indicates the desired frequency at which the sprite media handler should send idle events {events of type kOTIdleEvent) to the sprite track. In this case, the atom data is of type UInt32 and the default value is kNoOTIdleEvents {which means not to issue any idle events}. We'll consider these two sprite track properties in more detail in Chapter 16, "Wired."
Putting It All Together Let's see what the t~vo functions QTSprites AddlconMovieSamplesToMedia and QTSprites_AddPenguinMovieSamplesToMedia look like in their entirety. Listing 14.10 shows the definition of QTSprites AddlconMovieSamplesToMedia. m
Listing 14.10 Adding samples to the icon sprite movie. void QTSprites AddlconMovieSamplesToMedia (Media theMedia) m
QTAtomContainer QTAtomContainer RGBColor Point short short OSErr
426
Chapter 14 A Goofy Movie
mySample = NULL; mySpriteData= NULL; myKeyColor; myLocati on; isVisible; myLayer, mylndex, myCount; myErr = noErr;
/ / create a new, empty key frame sample myErr = QTNewAtomContainer(&mySample) ; i f (myErr ! = noErr) goto bai I ; myKeyColor.red = myKeyColor.green = myKeyColor.blue = Oxffff;
/ / white
/ / add images to the key frame sample Spri teUt i I s_AddPI CTImageToKeyFrameSample (mySampI e, kOldQTIconID, &myKeyColor, 1, NULL, NULL); Spri teUt i I s_AddPI CTImageToKeyFrameSampl e (mySampl e, kNewQTIconID, &myKeyColor, 2, NULL, NULL); / / add the i n i t i a l sprite properties to the key frame sample myErr = QTNewAtomContainer(&mySpri teData) ; i f (myErr ! = noErr) goto bai I ; / / the QT icon sprite myLocation.h = 32; myLocation.v = 32; isVisible = true; myLayer = -1; mylndex = 1; Spri teUti I s_SetSpri teData (mySpri teData, &myLocation, &isVisible, &myLayer, &mylndex, NULL, NULL, NULL); Spri teUti I s_AddSpri teToSampl e (mySample, mySpriteData, kQTIconSpri teAtomID) ; Spri teUt i I s_AddSpri teSampI eToMedia (theMedi a, mySampI e, kSpriteMediaFrameDurationlcon, true, NULL); / / add a few override samples to change the icon's location and image for (myCount = 1; myCount <= kNumOverrideSamples; myCount++) { QTRemoveChildren(mySample, kParentAtomlsContainer) ; QTRemoveChildren(mySpriteData, kParentAtomlsContainer) ; / / every frame, bump the icon's location myLocation.h += 2; / / change icon halfway through i f (myCount == kNumOverrideSamples / 2) mylndex = 2;
Creating Sprite Tracks 427
Spri teUti I s_SetSpri teData (mySpri teData, &myLocation, NULL, NULL, &mylndex, NULL, NULL, NULL); Spri teUti I s_AddSpri teToSampl e (mySample, mySpriteData, kQTIconSpri teAtomID) ; Spri teUt i I s_AddSpri teSampI eToMedia (theMedi a, mySampI e, kSpriteMediaFrameDurationlcon, false, NULL); bail: i f (mySample ! = NULL) QTDisposeAtomContai ner (mySample) ; i f (mySpriteData l= NULL) QTDisposeAtomContai ner (mySpri teData) ;
Listing 14.11 shows our complete definition of QTSprites_AddPenguinMovi eSampl esToMedi a.
Listing 14.11 Adding samples to the penguin sprite movie. void QTSprites AddPenguinMovieSamplesToMedia (Media theMedia) QTAtomContai ner mySample = NULL; QTAtomContainer mySpriteData= NULL; RGBColor myKeyCoI or; Point myLocat i on; short isVisible; short myLayer, mylndex, myCount; Modi fi erTrackGraph i csModeRecord myGraphi csMode; OSErr myErr = noErr; / / create a new, empty key frame sample myErr = QTNewAtomContainer (&mySample) ; i f (myErr ! = noErr) goto bai I ; myKeyColor.red = myKeyColor.green = myKeyColor.blue = Oxffff;
/ / whi te
/ / add images to the key frame sample Spri teUt i I s_AddPI CTImageToKeyFrameSample (mySample, kPenguinPictID, &myKeyColor, kPenguinlmagelndex, NULL, NULL);
428
Chapter 14 A Goofy Movie
/ / add the i n i t i a l sprite properties to the key frame sample myErr = QTNewAtomContainer(&mySpriteData) ; i f (myErr l= noErr) goto bai I ; / / the penguin sprite myLocation.h = O; myLocation.v = O; isVisible = true; myLayer = -1; myl ndex = 1; / / set the i n i t i a l blend amount (0 = f u l l y transparent; Oxffff = f u l l y opaque) myGraphi csMode,graphi csMode = blend; myGraphicsMode.opColor.red = O; myGraphicsMode.opColor.green = O; myGraphi csMode,opCol or. blue = O; Spri teUti I s SetSpri teData (mySpri teData, &myLocation, &isVisible, &myLayer, &mylndex, &myGraphicsMode, NULL, NULL); SpriteUtil s AddSpriteToSample(mySample, mySpriteData, kPenguinSpriteAtomID) ; SpriteUti I s_AddSpriteSampleToMedia(theMedia, mySample, kSpriteMediaFrameDurationPenguin, true, NULL); / / add a few override samples to change the penguin's opacity for (myCount = 1; myCount <= kNumOverrideSamples; myCount++) { QTRemoveChildren(mySample, kParentAtomlsContainer) ; QTRemoveChildren(mySpriteData, kParentAtomlsContainer) ; / / every frame, bump the penguin's opacity myGraphi csMode,graphi csMode = blend; myGraphicsMode.opColor.red = (myCount- 1) * (Oxffff / kNumOverrideSamples- I ) ; myGraphicsMode.opColor.green = (myCount- 1) * (Oxffff / kNumOverrideSamples- 1); myGraphicsMode.opColor.blue = (myCount- 1) * (Oxffff / kNumOverrideSamples- 1); SpriteUtils_SetSpriteData(mySpriteData, NULL, NULL, NULL, NULL, &myGraphicsMode, NULL, NULL); Spri teUti I s_AddSpri teToSample (mySample, mySpriteData, kPenguinSpri teAtomID) ; Spri teUti I s_AddSpri teSampl eToMedia (theMedi a, mySample, kSpri teMedi aFrameDurati onPengui n, fal se, NULL);
Creating Sprite Tracks 429
bail: i f (mySample I= NULL) QTDisposeAtomContai ner (mySampI e) ; i f (mySpriteData ! = NULL) QTDisposeAtomContai ner (mySpri teData) ;
For the definition of QTSprites_AddSpaceMovieSampl esToMedi a, see the file QTSpri tes.h. The basic strategy is the same, but the increased complexity of the space movie results in a considerably longer function.
Hit Testing As I mentioned earlier, QuickTime sprites can be interactive. That is to say, we can create movies with sprite tracks whose sprites respond to user actions like mouse movements and mouse button clicks. W h e n we consider wired sprites in Chapter 16, "Wired," we'll investigate the full power of this interactivity. In the meantime, let's take a look at a more limited form of interactivity supported by nonwired sprites, the ability to determine w h e t h e r the user has clicked on a sprite (also called hit testing). Programmatically finding clicks on a sprite is a two-stage process. First, we need to determine w h e n the user has clicked the mouse button inside of the movie rectangle. Then we need to determine w h e t h e r that mouse click was on top of a sprite. For the first task, we can add a case to the switch statement in our movie controller action filter function QTApp_MCActionFilterProc, looking for the mcActionMouseDown movie controller action (as shown in Listing 14.12). Listing 14.12 Detecting clicks in the movie window. switch (theActi on) { / / handle window resizing case mcActionControl lerSizeChanged: QTFrame_Si zeWindowToMovie (myWindowObject) ; break; / / handle idle events case mcActionldle: QTApp_Idl e ( (**myWi ndowObject). fWi ndow) ; break;
430
Chapter 14 A Goofy Movie
/ / handle mouse-down events. case mcActionMouseDown: isHandled = QTSprites HitTestSprites(myWindowObject, (EventRecord *)theParams) ; break; default: break;
The movie controller issues an mcActionMouseDown action w h e n e v e r it receives a mouse-down event that's inside the movie rectangle. The parameter data passed to our action filter function (in theParams) is a pointer to the event record for that mouse-down event. As you can see, our filter function simply calls the application-defined function QTSpri tes_HitTestSprites to see w h e t h e r the click is on a sprite and, if so, to react appropriately. (For more information about movie controller action filter functions, see Chapter 1, "It All Starts Today.") The sprite media handler supplies the SpriteMediaHitTest0neSprite and SpriteMediaHitTestAllSprites functions, which we can use to determine whether the user has clicked a sprite. For present purposes, we'll use Spri teMediaHitTestAllSprites, which looks at each one of the sprites in a sprite track to see whether it is currently at a specified location. SpriteMediaHi tTestA11Spri tes is declared essentially like this: ComponentResult Spri teMedi aHi tTestAl l Sprites (MediaHandler mh, long flags, Point loc, QTAtomID *spriteHitID);
If a sprite is situated at the specified location in the sprite track associated with the specified sprite media handler, then Spri teMedi aHi tTestAl 1Spri tes returns the ID of that sprite in the spriteHitIO parameter. If more than one sprite is situated at that location, then spriteHitIO is set to the ID of the frontmost sprite (that is, the sprite with the lowest layer number). If no sprite is situated at that location, then spriteHitIO is set to O. By default, the l oc parameter should be the location of the mouse click, in coordinates local to the sprite track. However, the event record passed to our movie controller action filter function contains the location of the mouse click in global coordinates. We could convert the global location to a local position, or we can add spriteHitTestLoclnDisplayCoordinates to the flags parameter, to indicate that the l oc parameter is in global coordinates. These are the flags currently understood by Spri teMedi aHi tTestA11Spri tes:
Hit Testing 4311
enum { spri teHi tTestBounds spri teHi tTest Image spri teHi tTest I nvi si bl eSpri tes spri teHi tTest I sCl i ck spri teHi tTest LocI nDi spl ayCoordi nates
};
= 1L << O, = 1L<<
1,
= 1L < < 2 , = 1L << 3 , = 1L<<4
If you want to accept clicks anywhere within the rectangular bounding box of a sprite, add in the spriteHitTestB0unds flag. If, conversely, you w a n t to accept clicks only on a nontransparent part of a sprite, add in the spri teHitTestImage flag. You must specify one or the other of these two flags, or else no hit testing will occur. Setting both of these flags is tantamount to setting only spri teHitTestlmage. (Earlier versions of QuickTime required you to set both flags if you wanted image testing, but current versions do not.) By default, SpriteMediaHitTestA1 ] Sprites and SpriteMediaHitTestOneSprite test only visible sprites for hits. If you want to test invisible sprites as well, set the s p r i t e H i t T e s t I n v i s i b l e S p r i t e s flag. If you want to pass the mouse click onto the codec that is rendering the sprite image, then specify the spriteHitTestIsClick flag; this is currently useful only with the Ripple codec. In QTSprites Hi tTestSprites, we want to test both visible and invisible sprites for hits, and we want to test only within the nontransparent portions of a sprite image. So we'll specify our flags parameter like this: myFlags = spriteHitTestlmage I spri teHi tTestLoclnDi spl ayCoordi nates I spri teHi tTest Invi si bl eSpri tes;
Now, what will we do w h e n we detect a click on a sprite? In theory, we can do anything we want. We've got the entire QuickTime API at our disposal, so we could do some neat things like launch the user's Web browser and navigate to a specific page, or download a file from a remote location, or even build a QuickTime movie. Even just within the context of our sprite track, we could do some rather interesting stuff, like moving the sprite to a new location in the sprite track, changing its image index, and so forth. For simplicity, we'll limit ourselves to changing the sprite's visibility state: if the sprite is currently visible, we'll make it invisible (and vice versa). Spri teMedi aGetSpri teProperty (myHandler, myAtomID, kSpritePropertyVisible, (void *)&isVisible) ; Spri teMedi aSetSpri teProperty (myHandler, myAtomID, kSpritePropertyVisible, (void *)!isVisible);
432
Chapter 14 A Goofy Movie
Listing 14.13 shows our complete definition of QTSpri tes_Hi tTestSpri tes.
Listing 14.13 Hit-testing sprites. Boolean QTSprites HitTestSprites (WindowObject theWindowObject, EventRecord *theEvent) m
ApplicationDataHdl MediaHandl er Boolean l ong QTAtomID Point ComponentResult
myAppData = NULL; myHandler = NULL; isHandled = false; myFl ags = OL; myAtomID = O; myPoint; myErr = noErr;
myAppData = (Appl i cati onDataHdl ) QTFrame_GetAppDataFromWi ndowObject (theWi ndowObject) ; i f (myAppData == NULL) goto bai I ; i f (theEvent == NULL) goto bai I ; / / make sure that the click is in our window i f ((**theWindowObject).fWindow I= QTFrame GetFrontMovieWindow()) goto bai I ; myHandler = (**myAppData). fSpri teHandl er; myFlags = spriteHitTestlmage I spriteHitTestLoclnDisplayCoordinates I spri teHi tTest Invi si bl eSpri tes; myPoint = theEvent->where; myErr = SpriteMediaHitTestAlISprites(myHandler, myFlags, myPoint, &myAtomID); i f ((myErr == noErr) && (myAtomID != 0)) { Boolean i sVi si bl e; / / the user has clicked on a sprite; / / for now, we'll just toggle the v i s i b i l i t y state of the sprite Spri teMedi aGetSpri teProperty (myHandler, myAtomID, kSpritePropertyVisible, (void *)&isVisible) ; Spri teMedi aSetSpri teProperty(myHandl er, myAtomID, kSpritePropertyVisible, (void * ) I i s V i s i b l e ) ;
Hit Testing 433
isHandled = true;
bail: return (i sHandl ed) ;
}
You'll notice that we need to make sure that the w i n d o w associated with the window object passed to QTSpri tes_Hi tTestSpri tes is the frontmost movie window, by executing these lines of code: if
((**theWindowObject).fWindow l : QTFrame_GetFrontMovieWindow()) goto bai I ;
This is because, w h e n we are handling an event on Macintosh computers, we call MClsPlayerEvent for each open movie controller, until we find one that handles the event. It's possible to have two or more overlapping sprite movies such that a mouse click does not hit a sprite in the frontmost movie w i n d o w but does hit one in an overlapped window. We can avoid unexpected behaviors by limiting our hit testing to the frontmost movie window. (On Windows, this additional check is unnecessary but harmless.)
Conclusion In this chapter, we've seen how to create and work with sprite tracks in QuickTime movies. Sprite tracks are remarkably easy to create. It's really just an exercise in creating atom containers and adding them as samples to a sprite media. Keep in mind that sprite media data isn't just a collection of pixels that are copied from the movie file onto the screen. Rather, sprites are distinct objects, with properties that can be changed dynamically to animate the sprites. This is what accounts for the drastic size differences between sprite and nonsprite versions of a movie. This is also what accounts for our ability to interact with individual sprites (for instance, our ability to hit-test mouse clicks on sprites). In the next chapter, we'll continue our investigation of sprites. Among other things, we'll see how to use a video track as the source of a sprite's image data, and we'll see how to use modifier tracks to animate sprites. Believe it or not, using modifier tracks instead of override samples will result in even further size reductions for our sprite movie files.
434
Chapter 14 A Goofy Movie
An Extremely Goofy Movie
Introduction In the previous chapter, we learned how to create some simple sprite movies. We saw that a sprite track typically contains two kinds of media samples, key frame samples and override samples. A key frame sample contains the set of images used by all the sprites in the track (up to the next key frame sample) and information about the initial properties of those sprites. An override sample contains only information about changes in the properties of the sprites. It's these changes that allow us to perform sprite animation with override samples. In this chapter, we're going to investigate two ways to perform sprite animation without using override samples. We'll see how to use a video track as the source for a sprite's images, and we'll see how to interpolate a sequence of values for a sprite property. Given a starting value and an ending value, QuickTime is able to figure out, for any m o m e n t in the duration of the animation, what the appropriate value between those two values should be. This process (touched on earlier in this book) is called tweening, and the track that contains the information needed to do the tweening is called a tween track. Video override tracks and tween tracks are two kinds of modifier tracks, or tracks whose media data is used to modify data in some other track. Modifier tracks do not display their data directly in a movie. Rather, that data is used only to supplement or alter the data in some other track in the movie. A video override track supplements the data in a sprite track by providing a source of images for one or more sprites in that track. And a tween track can modify the data in a sprite track by providing a sequence of settings for one of the properties of a sprite in that track. For example, we can use a tween track to generate a sequence of horizontal positions for a sprite.
435
Figure 15.1
The Test menu of OTSpritesPlus.
We'll begin by seeing how to use a video track as a source of image data for a sprite. Then we'll turn our attention to tweening. ~ e e n i n g is an extremely useful technique throughout QuickTime, not just in connection with sprite properties. So it will be good to spend some time getting comfortable building tween tracks. Our sample application for this chapter builds on the QTSprites application, so I've called it QTSpritesPlus. Figure 15.1 shows the Test menu of QTSpritesPlus. These menu items build movies that are modifications of the icon and penguin sprite movies that we built previously. The first menu item builds a movie that contains two sprites, and the image of one of those sprites is overridden by the frames from a video track. The next three menu items use tween tracks to perform various spatial animations on the icon sprite (moving it to the right, spinning it in place, and then both moving and spinning it at the same time). The last menu item builds the appearingpenguin movie once again, this time using a tween track to change the graphics mode of the penguin sprite image.
Video Override Tracks It's actually quite simple to use a video track as the source for a sprite's images. We need to add a video track to our sprite movie, create a track reference from the sprite track to the video track, and then indicate to the sprite track how it is to interpret the data that it receives from the video track. Figure 15.2 shows our video override movie. Here, the sprite track contains two sprites; the image of one of those sprites is a picture of the Titanium PowerBook G4, and the image of the other sprite has been replaced by the frames of the video track. By properly setting the positions of both sprites and choosing a video track with just the right dimensions, we can get that video track to exactly overlay the screen of the PowerBook.
436
Chapter 15 An Extremely Goofy Movie
Figure 15.2 A sprite image overridden by a video track.
Building a Sprite Track The first thing we need to do, of course, is build a new movie that contains a sprite track. In the present case, as just mentioned, we'll build a sprite track with a single key frame sample, which contains two sprites and two sprite images. What's different from the previous chapter is that we won't add any override frames to the sprite track. (In fact, if we w e r e to add some override samples that change the sprite image index, we would likely get some very strange results w h e n we ran the movie.) We'll set the duration of the key frame sample to 30 seconds (which is the duration of the video track we want to use as our image override track). Listing 15.1 shows the definition of the function QTSprites AddPowerBookMovieSamplesToMedia, which we use to
add the sample to the sprite track. Listing 15.1 Adding a single key frame sample to a sprite track. void QTSprites_AddPowerBookMovieSamplesToMedia (Media theMedia) QTAtomContai ner QTAtomContai ner RGBColor Point short OSErr
mySample = NULL; mySpri teData = NULL; myKeyColor; myLocation; isVisible, mylndex, myLayer; myErr = noErr;
Video Override Tracks 4 3 7
/ / create a new, empty key frame sample myErr = QTNewAtomContainer(&mySample) ; i f (myErr != noErr) goto bai I ; myKeyColor. red = myKeyColor. green = myKeyColor. blue = Oxffff;
/ / whi te
/ / add images to the key frame sample Spri teUt i I s AddPI CTImageToKeyFrameSampl e (mySample, kOldQTIconID, &myKeyColor, 1, NULL, NULL); Spri teUt i I s AddPI CTImageToKeyFrameSampl e (mySample, kTitaniumPowerBookID, &myKeyColor, 2, NULL, NULL); D
/ / add the i n i t i a l sprite properties to the key frame sample myErr = QTNewAtomContainer (&mySpri teData) ; i f (myErr ! = noErr) goto bai I ; / / the QT icon sprite myLocation, h = 46; myLocation, v = 8; isVisible = true; mylndex = kOl dQTIcon Imagel ndex; myLayer = 1; Spri teUti I s SetSpri teData (mySpri teData, &myLocation, &isVisible, &myLayer, &mylndex, NULL, NULL, NULL); Spri teUti I s_AddSpri teToSampl e(mySample, mySpriteData, kQTIconSpri teAtomID) ; / / the PowerBook sprite myLocation.h = O; myLocation, v = O; isVisible = true; mylndex = kPowerBooklmagelndex; myLayer = 2; Spri teUti I s_SetSpri teData (mySpri teData, &myLocation, &isVisible, &myLayer, &mylndex, NULL, NULL, NULL); Spri teUti I s_AddSpri teToSampl e (mySample, mySpriteData, kTi tani umPowerBookID) ; Spri teUti I s_AddSpri teSampl eToMedia (theMedi a, mySample, kSpriteMediaFrameDurationPowerBook, true, NULL);
438
Chapter 15 An Extremely Goofy Movie
bail: i f (mySample ! = NULL) QTDisposeAtomContai ner (mySample) ; i f (mySpriteData !: NULL) QTDisposeAtomContai ner (mySpri teData) ;
See the previous chapter for details on the sprite utilities (such as Spri teUti 1s_SetSpri teData) used in Listing 15.1.
Adding a Video Track Now let's add a video track to the sprite movie. We'll do this by copying an existing video track from another movie into the sprite movie. We want the video track to be at least as long as the sprite track, so that there are enough frames to replace the sprite image for the entire duration of the sprite track. For simplicity, we'll make the video track exactly as long as the sprite track. Figure 15.3 shows the track layout that we want to achieve. There are actually quite a n u m b e r of ways to use QuickTime APIs to copy a video track from one movie to another. We could, for instance, iterate through the samples in the source video track and call AddMediaSample to copy them one by one into a new track in the sprite movie. Or we could select the entire source movie and then call the AddMovieSelection function to insert the selection into the sprite movie. Or we could use the I n s e r t TrackSegment function to insert the entire source track into the sprite movie. That's the strategy we'll use in the QTSprites_ImportVideoTrack function, shown in Listing 15.2.
Figure 15.3 The structure of the video override movie.
Video Override Tracks 4 3 9
Listing
15.2 Copying a video track from one movie to another.
OSErr QTSprites_ImportVideoTrack (Movie theSrcMovie, Movie theDstMovie, Track *theTrack)
(
Track Medi a Track Media Fixed OSErr
mySrcTrack = NULL; mySrcMedi a = NULL; myDstTrack = NULL; myDstMedia = NULL; myWidth, myHeight; myErr = paramErr;
/ / get the f i r s t video track in the source movie mySrcTrack = GetMovielndTrackType(theSrcMovie, 1, VideoMediaType, movieTrackMediaType); i f (mySrcTrack == NULL) goto bai I ; / / get the track's media and dimensions mySrcMedia = GetTrackMedia(mySrcTrack) ; GetTrackDimensions(mySrcTrack, &myWidth, &myHeight); / / create a destination track myDstTrack = NewMovieTrack(theDstMovie, myWidth, myHeight, kNoVolume); i f (myDstTrack == NULL) goto bai I ; / / create a destination media myDstMedia = NewTrackMedia(myDstTrack, VideoMediaType, GetMediaTimeScale(mySrcMedia), O, O) ; i f (myDstMedia == NULL) goto bai I ; myErr = Begi nMediaEdi ts (myDstMedia) ; i f (myErr != noErr) goto bai I ; myErr = CopyTrackSettings(mySrcTrack, myDstTrack) ; i f (myErr != noErr) goto bai I ; myErr = InsertTrackSegment(mySrcTrack, myDstTrack, O, GetTrackDuration(mySrcTrack), O) ; i f (myErr ! = noErr) goto bai I ;
440
Chapter 15 An Extremely Goofy Movie
myErr = EndMediaEdi ts (myDstMedia) ; i f (myErr l= noErr) goto bai I ; bail: i f (theTrack i= NULL) *theTrack = myDstTrack; return (myErr) ;
Notice that we call BeginMediaEdits and EndMediaEdits so that the video
track media samples are copied into the sprite movie. If we didn't call these two functions, the video track in the sprite movie would reference the samples in the source movie. (There's nothing intrinsically wrong with that, but we generally prefer to create self-contained movies.) Notice also that QTSprites_ImportVideoTrack returns the new track that it creates to the caller. If our call to QTSprites_ImportVideoTrack returns successfully, we want to truncate the new video track so that it is exactly as long as the sprite track. We accomplish this by deleting any portion of the new video track that extends beyond the end of the sprite track, like this: myDuration = GetMovieDuration(theSpriteMovie) ; myErr = QTSprites_ImportVi deoTrack (myVi deoMovi e, theSpriteMovie, &myVideoTrack) ; i f (myErr ! = noErr) goto bai I ; DeleteMovieSegment (theSpriteMovie, myDuration, GetMovieDuration(theSpriteMovie) - myDuration) ;
Adding a Track Reference Now we need to establish a link between the video track and the sprite track to which it's going to send its data. We do this by creating a track reference from the sprite track to the video track. We first encountered track references in Chapter 10, "Word Is Out," w h e n we learned how to create chapter tracks. In the present case, we want to add a track reference of type kTrackReferenceModi f i e r from the sprite track to the new video track, like this: myErr = AddTrackReference(theSpriteTrack, myVideoTrack, kTrackReferenceModi f i e r , &myReflndex) ;
Video Override Tracks 441
W h e n QuickTime sees that the video track is the target of a reference of type kTrackReferenceModifier, it knows that the video track is a modifier track and hence not to draw the video track in the movie box. Instead, it sends the video data to the track that contains the reference to the video track.
Setting the Input Map But how does the sprite track know what to do with the data being sent to it from the video track? In particular, how does it know which sprite image is to be replaced by those video frames? This information is contained in a data structure called an input map that is attached to the track's media. A media's input map specifies how the track is to interpret any data being sent to it from a modifier track. In other words, w h e n e v e r a movie contains a modifier track, then some other track needs to have an input map that tells the associated media handler what to do with the data from the modifier track. The track reference and the input map work together to link a modifier track to its target track and to specify how the data from the modifier track should modify the target track. An input map is an atom container that contains one atom of type kTrackModi fierInput for each modifier track that is sending data to the target track. It's perfectly possible that several modifier tracks each send their data to a particular target track. In that case, the target track's input map would contain several kTrackModifierInput atoms. The ID of each such atom must be set to the reference index returned by AddTrackReference w h e n the track reference was created. Each atom of type kTrackModifierInput in an input map must contain at least two child atoms. One of these children is always of type kTrackModifierType and specifies the kind of data the target track is going to receive from the modifier track; in the case of a video override track, the type of the modifier track input is kTrackModi fierTypeImage. The file Movies.h defines constants for a large n u m b e r of modifier input types, several of which we'll encounter later in this chapter: enum { kTrackModi fi kTrackModi f i kTrackModi fi kTrackModi f i kTrackModi fi kTrackModi fi kTrackModi fi kTrackModi fi kTrackModi f i
442
erTypeMatri x erTypeCl i p erTypeGraphi csMode erTypeVol ume erTypeBal ance erTypelmage erObj ectMatri x erObj ectGraphi csMode erType3d4x4Matri x
Chapter 15 An Extremely Goofy Movie
=
1,
= 2, = 5, = 3, = 4,
= FOUR CHAR CODE('vide'), = B, = 7, = 8,
};
kTrackModi fi erCameraData kTrackModi fi erSoundLocal i zat i onData kTrackModi fi erObject ImageI ndex kTrackModi fi erObject Layer kTrackModi fi erObjectVi si bl e kTrackModi fi erAngl eAspectCam kTrackModi fi erPanAngle kTrackModifi erTi I tAngI e kTrackModi fi erVerti cal Fi el dOfViewAngle kTrackModi fi erObjectQTEventSend
= 9, = 10, = 11, = 12, = 13, = 14,
= = = =
FOURCHARCODE('pan '), FOURCHARCODE('tilt'), FOURCHARCODE('fov '), FOURCHARCODE('evnt~ m
m
The type of the second child atom in an input map entry atom depends on the kind of data specified in the first child atom. For instance, with a video override track, it specifies the index of the sprite image that is to be replaced by the frames of the video track. Figure 15.4 shows the structure of the input map we'll use to attach a video override track to a sprite track. Keep in mind that the sprite images in a key frame sample are stored in a list that is used by all the sprites in that track. This means that two or more sprites can have the same image {by having their image index property set to the same value}. So it's possible that two or more sprites can have their image replaced by the frames of a single video track. Similarly, it's possible for a single sprite to get its image data from first one and then another video track. In that case, there would have to be several override video modifier tracks and several kTrackModifierlnput atoms in the sprite track's input map. Listing 15.3 shows the definition of the QTSprites_AddVideoEntryToinput_ Map function, which we use to add the appropriate children to an existing input map.
I Atom container I
I
kTrackModifi erl nput Reference index
I kTrackModifi erType 1 kTrackModifi erTypelmage I
kSpritePropertylmagelndex I
1
Figure 15.4 The structure of an input map for video overrides. Video Override Tracks 4 4 3
Listing
15.3
Adding a video override entry to an input map.
OSErr QTSprites_AddVi deoEntryToInputMap (QTAtomContainer theInputMap, long theReflndex, long theID, OSType theType, char *theName)
{
#pragma unused(theName) QTAtom mylnputAtom; OSErr myErr = noErr; / / add an entry to the input map myErr = QTInsertChild(thelnputMap, kParentAtomlsContainer, kTrackModifierlnput, theRefIndex, O, O, NULL, &mylnputAtom) ; i f (myErr ! = noErr) goto bai I ; / / add two child atoms to the parent atom; / / these atoms define the type of the modifier input and the image index to override theType = EndianU32_NtoB(theType) ; myErr = QTInsertChild(thelnputMap, mylnputAtom, kTrackModifierType, 1, O, sizeof(OSType), &theType, NULL); i f (myErr ! = noErr) goto bai I ; theID = EndianS32 NtoB(theID); myErr = QTInsertChi]d(theInputMap, mylnputAtom, kSpritePropertylmagelndex, 1, O, sizeof(long), &theID, NULL); D
bail 9 return (myErr) ;
}
In QTSpritesPlus, we create an input map by calling QTNewAtomContainer: myErr = QTNewAtomContainer(&mylnputMap) ;
We create a n e w atom container because we created the sprite track and therefore know that it doesn't have an input map yet. Alternatively, we can retrieve a media's existing input map by calling fietMediaInputMap, like so: myErr = GetMediaInputMap (GetTrackMedi a (theSpri teTrack), &myInputMap) ;
A AA
Chapter 15 An Extremely Goofy Movie
In fact, it's probably preferable to call GetMedialnputMap always, since it will create a new input map for us if the specified media doesn't have one yet. Next we need to call our function QTSprites AddVideoEntryToInputMap to add the appropriate entries to the input map: myErr = QTSprites AddVideoEntryTolnputMap(myInputMap, myReflndex, kOl dQTIcon ImageIndex, kTrackModi fierTypeImage, NULL);
Finally, we attach the newly configured input map to the sprite track media by calling SetMedialnputMap: myErr = SetMedia InputMap (GetTrackMedi a (theSpri teTrack), myInputMap) ;
At this point, we can dispose of the atom container that we created (or that GetMedialnputMap created for us): QTDisposeAtomContai ner (myInputMap) ;
Our complete function for adding a video override track to an existing sprite movie is shown in Listing 15.4. Listing 15.4 Adding a video override track to a sprite movie. OSErr QTSprites AddVideoOverrideTrack (Movie theSpriteMovie, Track theSpriteTrack) Movie Track short short FSSpec OSType short QTFrameFi I eFi I terUPP TimeValue long QTAtomContai ner OSErr
myVideoMovie = NULL; myVideoTrack = NULL; myRefNum = klnval idFi leRefNum; myResID = O; myFSSpec; myTypeLi st [] = {kQTFileTypeMovie} ; myNumTypes = 2; myFileFilterUPP = NULL; myDurat i on; myReflndex; mylnputMap = NULL; myErr = noErr;
# i f TARGET OS MAC myNumTypes = O; #endi f n
Video Override Tracks 4 4 5
/ / have the user select a f i l e ; make sure i t has a video track in i t retry: myFi I eFi I terUPP = QTFrame_GetFiI eFi I terUPP((ProcPtr) QTFrame_Fi I terFi I es) ; myErr = QTFrame_GetOneFiI eWi thPrevi ew(myNumTypes, (QTFrameTypeListPtr)myTypeList, &myFSSpec, myFileFi IterUPP) ; i f (myFileFilterUPP ! = NULL) Di sposeNavObjectF i I terUPP (myFi I eFi I terUPP) ; if
(myErr I= noErr) goto bai I ;
myErr = OpenMovieFile(&myFSSpec, &myRefNum, fsRdPerm) ; i f (myErr ! = noErr) goto bai I ; / / now fetch the f i r s t movie from the f i l e myResI D = O; myErr = NewMovieFromFile(&myVideoMovie, myRefNum, &myResID, NULL, newMovieActive, NULL); i f (myErr i= noErr) goto bai I ; myVideoTrack = GetMovielndTrackType(myVideoMovie, 1, VideoMediaType, movieTrackMediaType) ; i f (myVideoTrack == NULL) goto retry; / / copy the video track into the sprite movie myDuration = GetMovieDuration(theSpriteMovie) ; myErr = QTSprites_ImportVideoTrack(myVideoMovie, theSpriteMovie, &myVideoTrack) ; i f (myErr ! = noErr) goto bai I ; / / truncate the new video track to the length of the sprite movie DeleteMovieSegment (theSpriteMovie, myDuration, GetMovieDuration(theSpriteMovie) - myDuration) ; / / attach the video track as a modifier to the sprite track / / create a media input map myErr : QTNewAtomContainer(&mylnputMap) ; i f (myErr ! = noErr) goto bai I ;
~1~6 Chapter 15 An Extremely Goofy Movie
myErr = AddTrackReference(theSpriteTrack, myVideoTrack, kTrackReferenceModi f i e r , &myReflndex) ; i f (myErr l= noErr) goto bai I ; myErr = QTSprites AddVideoEntryTolnputMap(mylnputMap, myReflndex, kOldQTIconlmagelndex, kTrackModifierTypelmage, NULL); i f (myErr l= noErr) goto bai I ; m
/ / attach the input map to the sprite track myErr = SetMedialnputMap(GetTrackMedia(theSpriteTrack), mylnputMap) ; bail: i f (myVideoMovie ! = NULL) Di sposeMovi e (myVi deoMovi e) ; i f (myRefNum ! = klnvalidFileRefNum) Cl oseMovi eFi I e (myRefNum) ; i f (mylnputMap ! = NULL) QTDisposeAtomContai ner(mylnputMap) ; return (myErr) ;
So it really is fairly straightforward to use a video track as the source of a sprite's images. It's mostly just a matter of linking the video track and the sprite track in the correct manner, using a track reference and an input map. As we'll see shortly, we need to perform this same linkage between a sprite track and a tween track that's sending it data.
l
eening ~ e e n i n g is the process of generating values that lie between two given values (an initial value and a final valuel or that are in some other way algorithmically derived from some given data. When it was first introduced (in QuickTime version 2.5), the tween media handler supported only linear interpolation between initial and final values. That is to say, if (for instancel the initial and final values are integers, then the tweened values all lie on a straight line drawn between those two values, as illustrated in Figure 15.5. If the value at time 0 is, say, 10 and the value at time 30 is 30, then the tweened value at time 15 will be 20.
Tweening 447
3020
10I
I
I
Time Figure 15.5 Derivinga tween value from initial and final values. 0
10
20
30
The tween media handler isn't limited to tweening only integer values, however. In QuickTime version 2.5, the tween media handler could work with a large number of types of data, defined by these constants: enum { kTweenTy )eShort
};
= 1,
kTweenTy )eLong
= 2,
kTweenTy )eFixed
= 3,
kTweenTy )ePoi nt
= 4,
kTweenTy ~eQDRect
= 5,
kTweenTy )eQDRegi on
= 6,
kTweenTy )eMatri x
= 7,
kTweenTy )eRGBCol or
= 8,
kTweenTy ~eGraphi csModeWi thRGBCol or
= 9,
kTweenTy! )e3dScale
= '3sca',
kTweenTy )e3dTransl ate
= ' 3tra',
kTweenTy )e3dRotate
= '3rot',
kTweenTy }e3dRotateAboutPoi nt
= ' 3rap',
kTweenTy )e3dRotateAboutAxi s
= ' 3rax',
kTweenTy )e3dQuaternion
= ' 3qua',
kTweenTy )e3dMatri x
= ' 3mat',
kTweenTy )e3dCameraData
= '3cam',
kTweenTy )e3dSoundLocal i z a t i o n D a t a
= '3slc'
For instance, given two structures of type RGBColor, the tween media handler can generate a third RGBColor structure by interpolating the individual fields of the initial and final structures. In all the cases just listed, linear interpolation is used, although not all fields of a structure are always interpolated, as we'll see later. (We shall ignore the 3D tween types, as they are used to tween 3D tracks, which are not currently supported in Mac OS X.) QuickTime 3.0 added support for another few handfuls of data types, defined by these constants:
448
Chapter 15 An Extremely Goofy Movie
enum { kTweenTypeQTFl oatSi ngl e kTweenTypeQTFl oatDoubl e kTweenTypeFi xedPoi nt kTweenTypePathToMatrixTranslation kTweenTypePathToMatrixRotati on kTweenTypePathToMatrixTranslationAndRotation kTweenTypePathToFixedPoint kTweenTypePathXtoY kTweenTypePathYtoX kTweenTypeAtomList kTweenTypePolygon kTweenTypeMultiMatrix kTweenTypeSpin kTweenType3dMatrixNonLinear kTweenType3dVRObject
};
= = = = = = = = = = = = = = =
10, 11, 12, FOUR CHAR CODE( gxmt'), FOUR CHAR CODE( gxpr ), FOUR CHAR CODE( gxmr ), FOUR CHAR CODE( gxfp ), FOUR CHAR CODE( gxxy ), FOURCHAR CODE( gxyx ), FOUR CHAR CODE( atom ), FOUR CHAR CODE( poly ), FOURCHAR CODE('mulm ), FOUR CHAR CODE('spin ), FOUR CHAR CODE('3nlr ), FOUR CHAR CODE('3vro ) m
B
D
These new tween types provide support for more complex tweening operations and for operations that are not simple linear interpolations of data. For instance, the various types of path tweens allow us to derive values based on the shape of an arbitrary curve defined by a vector path. And the list tween derives values from a list of atoms in an atom container, which can result in a series of discrete steps of noncontinuous values. In the remainder of this chapter, we'll investigate four of these tween types in detail: kTweenTypeGraphicsModeWithRGBColor, kTweenTypeMatrix, kTweenTypeSpin, and kTweenTypeMultiMatrix.
Graphics Mode Tweening Let's begin our hands-on work with tweening by reconsidering our old favorite, the appearing-penguin movie. In the previous chapter, we built a sprite version of this movie by creating a sprite track with one key frame sample {which holds the compressed penguin image and the initial sprite properties) and 99 override frames (which hold data of type ModifierTrackGraphicsModeRecord). We constructed the override samples so that the opacity of the sprite smoothly increases from total transparency to total opacity. This is a textbook case of where tweening can be of assistance. Instead of adding 99 override samples to the sprite track, we can instead add a single tween track to the movie that effectively says: start the graphics mode of the sprite image at total transparency and smoothly increase it up to total opacity. A couple of pictures will help us appreciate the difference here. Figure 15.6 shows the original structure of the penguin sprite movie. Figure 15.7
Graphics Mode Tweening 449
Figure 15.6 The structure of the original penguin sprite movie.
Figure 15.7 The structure of the revised penguin sprite movie.
shows the revised version, which uses a tween track in place of the override samples. The start time and duration of the tween media sample determine the start time and duration of the tweening operation. As we'll see later, however, it's possible to limit the tweening to only part of the time spanned by the tween media sample.
Adding a "l~een Track We add a tween track to our sprite movie in the standard way, by calling NewMovieTrack and NewTrackMedia: myTweenTrack = NewMovieTrack(theMovie, O, O, kNoVolume); myTweenMedia = NewTrackMedia (myTweenTrack, TweenMediaType, GetMovieTimeScale(theMovie), NULL, O) ;
Now we need to add a t w e e n media sample to the t w e e n track. A t w e e n media sample is an atom container that contains one or more tween entries. A tween entry is an atom (of type kTweenEntry) that holds other atoms, including at least a tween type atom (of type kTweenType) and a tween data atom (of type kTweenData). Figure 15.8 shows the general structure of a tween media sample. 450
Chapter 15 An Extremely Goofy Movie
Atom container ] kTweenEntry
I
~ kTweenType 1
Tween type ~ kTweenData
]
1
]
Tween data
[
Figure 15.8 The structure of a tween media sample.
The atom data in a tween t2cpe atom is the type of the tween atom (that is, one of the constants listed earlier). The kind of atom data in a tween data atom depends on the tween type; for instance, for a tween entry of type kTweenTypeGraphicsModeWithRGBColor, the atom data is a pair of ModifierTrackGraphicsModeRecord structures that specify the initial and final graphics modes. Listing 15.5 shows how we can build the tween media sample for our penguin movie. Listing 15.5 Building a graphics mode tween media sample. Modi fi erTrackGraphi csModeRecord QTAtomContainer QTAtom
myGraphicsMode[2] ; mySample = NULL; myTweenEntryAtom = O;
myErr = QTNewAtomContainer (&mySample) ; i f (myErr ! = noErr) goto bai I ; / / add a tween entry atom to the atom container myErr = QTInsertChild(mySample, kParentAtomlsContainer, kTweenEntry, 1, O, O, NULL, &myTweenEntryAtom); i f (myErr ! = noErr) goto bai I ; / / set the type of this tween entry myType = EndianU32_NtoB(kTweenTypeGraphicsModeWithRGBColor) ; myErr = QTInsertChild(mySample, myTweenEntryAtom, kTweenType, 1, O, sizeof(myType), &myType, NULL); i f (myErr ! = noErr) goto bai I ;
Graphics ModeTweening
451
/ / set the i n i t i a l blend amount (0 = f u l l y transparent) myGraphicsMode[O] .graphicsMode = EndianU32_NtoB(blend) ; myGraphicsMode[O] .opColor.red = O; myGraphi csMode[0]. opCol or. green = O; myGraphicsMode[O] .opColor.blue = O; / / set the final blend amount (Oxffff = f u l l y opaque) myGraphi csMode[ 1]. graph i csMode = EndianU32_NtoB(b I end) ; myGraphicsMode[1] .opColor. red = EndianU16_NtoB(Oxffff) ; myGraphicsMode[1] .opColor.green = EndianU16_NtoB(Oxffff) ; myGraphicsMode[1] .opColor.blue = EndianU16 NtoB(Oxffff) ; myErr = QTInsertChild(mySample, myTweenEntryAtom, kTweenData, 1, O, 2 * sizeof(ModifierTrackGraphicsModeRecord), myGraphicsMode, NULL);
As you can see, the first record specifies a fully transparent graphics mode, while the second specifies a fully opaque graphics mode. The tween media handler generates a series of records by interpolating the red, green, and blue fields. The graphicsMode field is not interpolated; rather, it is copied from the first record into the tweened record. Now it's time to add the sample to the tween media. The tween media handler uses the generic sample description, of type SampleDescription. So we need to create a sample description, fill in the descSize field, and then call AddMediaSample, as shown in Listing 15.6.
Listing
15.6 Adding a sample to a tween media.
/ / create the sample description mySampleDesc = (SampleDescriptionHandle)NewHandleClear(sizeof(SampleDescription)) ; i f (mySampleDesc == NULL) goto bai I ; (**mySampleDesc) .descSize = sizeof(SampleDescription) ; / / add the tween sample to the media myErr = BeginMediaEdi ts (myTweenMedia) ; i f (myErr ! = noErr) goto bai I ; myErr = AddMediaSample(myTweenMedia, mySample, O, GetHandleSize(mySample), GetMedi aDurat i on (GetTrac kMedi a (t heTargetTrack) ), (SampleDescriptionHandle)mySampleDesc, 1, O, NULL);
452
Chapter 15 An Extremely Goofy Movie
i f (myErr I= noErr) goto bai I ; myErr = EndMediaEdi ts (myTweenMedia) ;
Notice that we specify the duration to AddMediaSampl e like this: GetMedi aDurat i on (GetTrac kMedi a (t heTarget Track) )
This means that the tween media sample extends for the entire length of the sprite track (as shown in Figure 15.7). Finally, we need to insert the media into the tween track: myErr = InsertMedialntoTrack(myTweenTrack, O, O, GetMediaDuration(myTweenMedia), fixed1) ;
Setting the Input Map We've finished creating the tween track, but we still need to link it up with the sprite track so that when the movie is played back, the tween track sends its output (a record of type ModifierTrackGraphicsModeRecord) to the sprite track. The sprite track will use that output to set the graphics mode property of some sprite in the sprite track. We indicate both of these kinds of information (the type of modifier data the tween track is producing and the ID of the sprite to apply it to) in the sprite track's input map. We create an input map in exactly the same way we did when working with a video override track, by calling QTNewAtomContainer: myErr = QTNewAtomContainer (&myI nputMap) ;
Then we need to add a track reference, from the target track {the sprite track) to the tween track: myErr = AddTrackReference(theTargetTrack, myTweenTrack, kTrackReferenceModi f i e r , &myReflndex) ;
And then we need to add some data to the empty input map. QTSpritesPlus calls another application-defined function, QTSprites_AddTweenEntryToInputMap, passing in the input map, the track reference index obtained from AddTrackReference, the ID of the sprite to tween, and type of the modifier data:
Graphics Mode Tweening 453
myErr = QTSprites_AddTweenEntryTolnputMap(mylnputMap, myReflndex, kPenguinSpriteAtomID, kTrackModifierObjectGraphicsMode, NULL); QTSprites_AddTweenEntryTolnputMap is defined in Listing 15.7. It's pretty much identical to QTSprites_AddVideoEntryTolnputMap, except that the ID
passed in now specifies the ID of the sprite to which the tween data is to be applied, not the image index of the sprite. Accordingly, the second child atom we add to the input map is of type kTrackModifierObjectID, not kSpri tePropertyImagel ndex.
Listing 15.7 Adding a tween entry to an input map. OSErr QTSprites_AddTweenEntryTolnputMap (QTAtomContainer thelnputMap, long theRefIndex, long theID, OSType theType, char *theName)
{
#pragma unused(theName) QTAtom mylnputAtom; OSErr myErr = noErr; / / add an entry to the input map myErr = QTInsertChild(thelnputMap, kParentAtomlsContainer, kTrackModifierlnput, theReflndex, O, O, NULL, &mylnputAtom); i f (myErr ! = noErr) goto bai I ; / / add two child atoms to the parent atom; theType = EndianU32 NtoB(theType); myErr = QTInsertChild(thelnputMap, mylnputAtom, kTrackModifierType, 1, O, sizeof(OSType), &theType, NULL); i f (myErr ! = noErr) goto bai I ; theID = EndianU32 NtoB(theID); myErr = QTInsertChild(thelnputMap, mylnputAtom, kTrackModifierObjectID, 1, O, sizeof(long), &theID, NULL); m
bail 9 return (myErr) ;
}
Finally, we need to set the sprite track's input map, by calling SetMedialn-
putMap"
myErr = SetMedialnputMap(GetTrackMedia (theTargetTrack), mylnputMap) ;
454
Chapter 15 An Extremely Goofy Movie
Vo.il~, we've just created our first tween track and linked it to the sprite track. If we save the movie to disk (by calling AddMovieResource and C]oseMovieFile) and then reopen and play it, we'll see the same sequence of frames that we saw w h e n playing the original penguin movie or the original sprite version of the penguin movie. There are, however, several important advantages to using a tween track in place of sprite track override samples. First of all, since we have stored only two ModifierTrackGraphicsModeRecord structures instead of 99, we can expect to see some reduction in the size of the movie file. In fact, the size of the penguin movie file is reduced from 36 Kbytes to 28 Kbytes (almost one-fourth smaller). The second important advantage to using a tween track is that it has no preestablished frame rate. In the override sample version of the penguin movie, we'll get at most 10 frames per second (fps), because we have 100 frames in a 10-second movie. (I say "at most" 10 fps because QuickTime might need to drop some frames if the user's computer is not capable of displaying 10 fps.) With the tween track version, we'll get as many frames per second as QuickTime and the accompanying hardware can m a n a g e - - w h i c h should easily surpass 10 fps. (Indeed, on a midrange PowerMac G3, I clocked the tween version of the penguin movie at a brisk 83 fps!) So when, in the previous paragraph, I said that we'll see "the same sequence of frames," I was fibbing; in fact, we're likely to get a much better visual output with the tween track movie than with the override sample movie. Smaller and better; ain't life grand?
Matrix Tweening Now that you're convinced that tween tracks are worth playing with, let's consider a few more examples. In the previous chapter, we changed the horizontal position of the icon sprite by adding a bunch of override samples, each of which contained a matrix record specifying a new position. Once again, we can replace all those override samples with a single tween track, which contains the initial and final matrices. At run time, the tween media handler generates the intermediate matrices and funnels them to the sprite media handler, which applies them to the sprite to achieve the horizontal motion.
Building the T w e e n Data A t o m Listing 15.8 shows the code that we use to build the tween data atom in the media sample for the tween track. The atom contains two matrices, one for the initial position of the icon sprite and a second for its final position. Note that the matrices are just concatenated together as the atom data, not inserted into separate atoms.
Matrix Tweening 455
Listing 1S.8 Building a matrix tween media sample. MatrixRecord
myMatrix [2] ;
/ / set the i n i t i a l data for this tween entry Set I dent i tyMatri x (&myMatri x [0] ) ; Trans I ateMat ri x (&myMatri x [0], Long2Fix(klconDimension + (klconDimension / 2)), Long2Fix(klconDimension + (klconDimension / 2))) ; EndianUtils MatrixRecord NtoB(&myMatrix[O]) ; / / set the final data for this tween entry Set I dent i tyMat ri x (&myMatri x [ 1] ) ; Transl ateMatri x (&myMatri x [1], Long2Fix(230 + (klconDimension / 2)), Long2Fix(klconDimension + (klconDimension / 2))); EndianUtils MatrixRecord NtoB(&myMatrix[1]); myErr = QTInsertChild(mySample, myTweenEntryAtom, kTweenData, 1, O, 2 * sizeof(MatrixRecord), myMatrix, NULL);
The tween track is built exactly as shown; the only difference here is that the modifier input type is kTrackModifierObjectMatrix. Once again, the resulting movie file is smaller than the version that contains override samples. Also, thanks to the increase in frames per second, the movie playback is noticeably smoother than the override samples version.
Setting the Tween Offset and Duration A tween operation begins at the start of a tween media sample and extends for the entire length of the sample. It's possible, however, to add a couple of atoms to a tween entry atom to modify this default behavior. If we want a tweening operation to begin at some time after the start of a media sample, we can include a tween offset atom, of type kTweenStartOffset. The data for a tween offset atom is a value of type TimeValue that indicates how far into the tween media sample the tweening operation is to begin. (This value should be specified in the media's time scale.) If theSample is a tween media sample atom container and myAtom is a tween entry atom, then we can add a tween offset atom to that tween entry atom like this: theOffset = EndianS32 NtoB(theOffset) ; myErr = QTInsertChild(theSample, myAtom, kTweenStartOffset, 1, 1, sizeof(TimeValue), &theOffset, NULL); B
45G
Chapter 15 An Extremely Goofy Movie
The index and ID of the tween offset atom must both be 1. If we want to modify the duration of a tweening operation, we can add a tween duration atom, of type kTweenDuration, to a tween entry atom. The data for a tween duration atom is a value of type TimeValue. We can add a tween duration atom to a tween entry atom like this: theDuration = EndianS32 NtoB(theDuration) ; myErr = QTInsertChild(theSample, myAtom, kTweenDuration, 1, 1, sizeof(TimeValue), &theDuration, NULL);
Once again, the index and ID of the tween duration atom must both be 1. The file QTSpritesPlus.c defines two functions, QTSprites_SetTweenEntryS t a r t 0 f f s e t and QTSprites_SetTweenEntryDuration, that you can use to set a tween's offset and duration.
Spin Tweening Suppose now that we want a sprite to spin around in the sprite track. A standard way to get a sprite to spin is to include a large n u m b e r of sprite images in the key frame sample and to change the sprite's image index during playback. Or, we could try to use the matrix tween just discussed, in conjunction with the RotateMatrix function, to set up a rotation matrix for the sprite. It turns out, however, that this is trickier than you'd suspect. If we w a n t e d to rotate the icon one full turn, we might think we could specify a final matrix like this: Set I dent i tyMat ri x ( &myMatri x [ 1] ) ; RotateMatrix(&myMatrix[1], Long2Fix(360), O, 0);
But that w o n ' t do at all; 360 ~ is the same position as 0 ~ so the resulting tween would effectively do nothing. Worse yet, it's not clear how w e ' d specify rotating two or more times. No doubt we could chop things up into a n u m b e r of smaller rotations, but that's getting unduly messy. To allow us to easily spin an object an arbitrary n u m b e r of rotations, QuickTime 3.0 introduced spin tweening. The tween atom data for a spin tween consists of two Fixed values, the initial rotation a m o u n t and the total n u m b e r of rotations. Listing 15.9 shows part of our code for building a tween track sample that spins the QuickTime icon kNumRotations times (which is defined in QTSpritesPlus.h as 5).
Spin Tweening 457
Listing 15.9 Building a spin tween media sample. Fixed
mySpi nData [2] ;
/ / set the i n i t i a l rotation value mySpinData[O] = EndianU32 NtoB(O); / / set the number of rotations mySpinData[1] = EndianU32 NtoB(Long2Fix(kNumRotations)) ; myErr = QTInsertChild(mySample, myTweenEntryAtom, kTweenData, 1, O, 2 * sizeof(Fixed), mySpinData, NULL);
The output of a spin tweening operation is a matrix (os type MatrixRecord) that can be applied to a sprite to achieve the desired rotation. The point os rotation (that is, the point about which the sprite image is rotated) is the image's registration point, which is set at the time the sprite image is added to the key frame. Note that the registration point is not a sprite property and cannot, therefore, be changed dynamically. Rather, the registration point is a property of the sprite image. The default registration point is (0, 0), or the upper-left corner of the box surrounding the sprite image. Figure 15.9 shows a sprite movie in which the QuickTime icon rotates about the default registration point. Notice that the icon is now located at track position (0, 0). That's because the matrix generated by the spin tween overrides the matrix in the key frame sample (with which we specified the desired position of the icon). When we run the movie, the icon spins around the point (0, 0), which means that it keeps moving in and out of the movie box. Now that's goofy.
Figure 15.9 A sprite spinning around the default registration point.
458
Chapter 15 An Extremely Goofy Movie
Figure 15.10 A sprite spinning around a new registration point.
Let's move the registration point of the icon image to the center of the image, like so: myPoint.x = Long2Fix(klconDimension / 2); myPoint.y = Long2Fix(klconDimension / 2); / / add images to the key frame sample Spri teUt i I s_AddPI CTImageToKeyFrameSample (mySample, kOldQTIconID, &myKeyColor, 1, &myPoint, NULL);
This gives us a movie that's slightly better, but still not w h o l l y satisfactory (Figure 15.10). The icon is still stuck in the upper-left corner of the movie box. What w e ' d like, I think, is for the icon to be situated at some other location in the movie box and to spin around its registration point at that location.
Multimatrix Tweens What we need is to be able to generate a matrix that both translates and spins the sprite image. This is a job for the rnultimatrix tween, introduced in QuickTime 3.0. A multimatrix tween concatenates two or more matrices produced by other kinds of tweens. To create a multimatrix tween, we build the atoms for the individual tweens and then add t h e m to the data atom of a multimatrix tween entry. Figure 15.11 shows the atom structure that we want to build. Notice that the tween data atom of the multimatrix tween entry is a parent atom that contains two tween entry atoms, one for the tween that spins the sprite image and one for the tween that translates the sprite image.
Multimatrix Tweens
459
Atomcontainer J kTweenEntry I
I
kTweenType 1 kTweenTypeMuI t i Matr i x kTweenData 1
kTweenEntry 1
~ kTweenType 1 kTweenTypeSpi n
t kTweenData
1 Two Fixed values
Figure
kTweenEntry 2 I kTweenType 1 kTweenTypeMat r i x
t kTweenData
1 Two MatrixRecords
15.11 A multimatrixtweenthat translatesand spins. Listing 15.10 gives the code we use to add atoms to a p a r e n t atom (myTweenEntryAtom) to build a m u l t i m a t r i x t w e e n atom. Matrix operations are not in general commutative, so the order in w h i c h we add the t w e e n entry atoms for the matrix t w e e n s to the m u l t i m a t r i x t w e e n data atom is important. In Listing 15.10, we add the spin t w e e n and then the translation tween.
Listing
15.10 Buildinga multimatrixtween mediasample.
QTAtom QTAtom Matri xRecord Fixed
myMultiTweenDataAtom = O; myAtom = O; myMatrix [2] ; mySpinDatar2];
// add a multimatrix tween data atom to the tween entry atom myErr = QTInsertChild(mySample, myTweenEntryAtom, kTweenData, 1, O, O, NULL, &myMultiTweenDataAtom);
460 Chapter15 An Extremely Goofy Movie
i f (myErr I= noErr) goto bai I ; / / add a spin tween to the multimatrix tween data atom myErr = QTInsertChild(mySample, myMultiTweenDataAtom, kTweenEntry, 1, O, O, NULL, &myAtom); i f (myErr l= noErr) goto bai I ; / / set the type of this tween entry myType = EndianU32 NtoB(kTweenTypeSpin) ; myErr = QTInsertChild(mySample, myAtom, kTweenType, 1, O, sizeof(myType), &myType, NULL); i f (myErr ! = noErr) goto bai I ; / / set the i n i t i a l rotation value mySpinData[O] = EndianU32 NtoB(O); / / set the number of rotations mySpinData[1] = EndianU32 NtoB(Long2Fix(kNumRotations)) ; myErr = QTInsertChild(mySample, myAtom, kTweenData, 1, O, 2 * sizeof(Fixed), mySpinData, NULL); i f (myErr ]= noErr) goto bai I ; / / add a translation matrix tween to the multimatrix tween data atom myErr = QTInsertChild(mySample, myMultiTweenDataAtom, kTweenEntry, 2, O, O, NULL, &myAtom); i f (myErr I= noErr) goto bai I ; / / set the type of this tween entry myType = EndianU32 NtoB(kTweenTypeMatrix) ; myErr = QTInsertChild(mySample, myAtom, kTweenType, 1, O, sizeof(myType), &myType, NULL); i f (myErr l= noErr) goto bai I ; / / set the i n i t i a l data for this tween entry Set I dent i tyMat ri x (&myMatri x [0] ) ; Transl ateMatri x (&myMatrix [0], Long2Fix(klconDimension + (klconDimension / 2)), Long2Fix(klconDimension + (klconDimension / 2))) ; EndianUtils MatrixRecord NtoB(&myMatrix[O]);
Multimatrix Tweens 4611
/ / set the final data for this tween entry Set I dent i tyMat ri x (&myMatri x [ 1] ) ; Transl ateMatri x (&myMatri x [1], Long2Fix(230 + (klconDimension / 2)), Long2Fix(klconDimension + (klconDimension / 2))); EndianUtils MatrixRecord NtoB(&myMatrix[1]); myErr = QTlnsertChild(mySample, myAtom, kTweenData, 1, O, 2 * sizeof(MatrixRecord), myMatrix, NULL);
Figure 15.12 shows a frame of the resulting movie, in which the icon rolls its way from left to right. Now that's extremely goofy.
Figure 15.12
A sprite rolling across the movie box.
Conclusion Video override tracks and tween tracks provide two different ways for us to enhance our sprite movies. Video override tracks give us a way to tap into an existing video track as a source of images for a sprite; they are kind of a "one-trick pony," but nonetheless useful in the right circumstances. 'I~een tracks, by contrast, are quite generally useful; they give us a way to algorithmically alter sprite properties without using override samples. ~ e e n tracks have the added benefits of smaller file sizes and increased frame rates. They are also usually easier to construct than a sequence of override samples. Why, after all, should we bother to do the math to figure out how to fade from full transparency to full opacity? Let's just let the tween media handler do it for us. In the next chapter, we're going to consider yet another way to enhance our sprite movies, by attaching wired actions to the sprites. Our goal is not only to make our sprites active (using override samples or video override tracks or tween tracks), but also to make them interactive.
462
Chapter 15 An Extremely Goofy Movie
Wired
Introduction In the past two chapters, we've learned how to create movies with sprite tracks. We've also learned several ways to animate the sprites in those tracks, first by adding override samples to the sprite track, and then by adding a video override track or a tween track to the movie. In both cases, we animate the sprites by changing one or more of the sprite properties (image index, matrix, graphics mode, and so forth). Our sprite animations so far have been pretty simple, but in fact we now have the ability to create some fairly complex sprite animations. For instance, we could add several tween tracks to a sprite movie, one that changes a sprite's position (by tweening its matrix} and another that fades the sprite in and out (by tweening its graphics mode). Throw in a background sprite and a video override track and we have all the makings of a reasonably compelling animation. What we don't have, yet, is any way for the user to interact with the animation [other than simply starting it and stopping it using the controller bar). To be truly compelling, our movies should allow the user to manipulate sprites in the movie, or trigger changes in the movie by clicking a sprite, or maybe even open a website in a browser window w h e n the user moves the cursor over a sprite. All of this is possible using one of QuickTime's most powerful features: wired actions. In this chapter, we're going to see how to attach (or "wire") interactive, dynamic behaviors (that is, "actions") to parts of a sprite movie. Consider, for example, the movie shown in Figure 16.1. This looks just like the moving icon movies that we constructed last time [except that there is no controller bar on the movie window). But appearances can be deceiving, for lurking within the movie are several new and interesting behaviors. When we move the cursor over the icon sprite, the cursor automatically changes into an open-hand cursor [Figure 16.2), which might suggest that
463
Figure 16.1 An icon sprite movie.
Figure 16.2 The cursor changing shape inside the sprite.
Figure 16.3 The sprite being dragged around in a sprite movie. we can drag the icon around. Indeed we can: if we hold down the mouse button and drag, the sprite is dragged along with the cursor, which changes into the closed-hand cursor (as shown in Figure 16.3); w h e n we release the mouse button, the icon sprite remains at that new position. Or consider the movie shown in Figure 16.4. This movie consists of a video track, a sound track, and a sprite track. There are six sprites in the sprite track, and each sprite is wired to duplicate one of the behaviors of the
464
Chapter 16 Wired
Figure 16.4
A sprite track that controls a video track.
buttons in the standard controller bar. (From left to right, clicking the sprites performs these actions: go to the beginning of the movie, go backward one frame, start the movie playing, pause the movie, go forward one frame, and go to the end of the movie.) As you can see, the movie has no controller bar, since we can now control the movie using the sprite "buttons" alone. You might be wondering what's so special about all of this. After all, in our first discussion of sprites (see Chapter 14, "A Goofy Movie"), we saw how to move sprites around and respond to mouse button clicks on them. At that time, all we did was change the visibility state of the sprite, but it would have been easy for us instead to repeatedly change the position of the sprite until the mouse button was released (and hence drag it around the movie). The important difference is that there we used a special application (QTSprites) to supply those capabilities, whereas here we shall build a movie file that can be opened by any QuickTime-savvy application and that will have the same behaviors in that application as well. In other words, the dynamic, interactive behaviors are stored in the movie file as part of the media data, not supplied by the playback application. Our sample application for this chapter is called QTWiredSpritesJr (because it's a stripped-down version of an existing sample code package called QTWiredSprites). Figure 16.5 shows the Test menu of QTWiredSpritesJr. The two menu items allow us to add the controller sprite track to a QuickTime movie and to create the movie with a draggable sprite shown in Figure 16.1. This might not seem like a lot, but it will occupy us for quite a while. And we're going to encounter some pretty amazing stuff along the
Introduction
41.65
Figure 16.5 The Test menu of QTWiredSpritesJr. way. So sit back and take a deep breath. And get r e a d y to add a really powerful tool to your QuickTime toolkit.
Events, P a r a m e t e r s , Actions A wired sprite is a sprite to w h i c h one or more event atoms have b e e n attached. An event atom is a QuickTime atom that associates an event w i t h one or more actions that are triggered w h e n that event occurs. Each action can have one or more parameters, w h i c h specify any additional information that's necessary for the action to be performed. In addition, an action m a y have a target, w h i c h is the object upon w h i c h the action is performed. The structure of a typical event atom is s h o w n in Figure 16.6. Here, the event atom contains a single child atom, of type kActi on. This action atom, in turn, contains two children, one that specifies the type of the action and a n o t h e r that specifies the single p a r a m e t e r to that action. Some actions have m o r e than one parameter, and some have none. An event atom is attached to a particular sprite by including the atom as a child of the sprite atom (that is, the atom of type kSpri teAtomType) and hence as a sibling of the sprite property atoms. Notice that the event type is the atom ID of the event atom.
Specifying Events W h e n wired sprites were first introduced (in Q u i c k T i m e 3.0), there w e r e seven basic types of events that could trigger wired sprite actions, defined by these constants: enum { kQTEventMouseCl i ck kQTEventMouseCl i ckEnd kQTEventMouseCl i ckEndTri ggerButton kQTEventMouseEnter kQTEventMouseExi t kQTEventFrameLoaded kQTEventIdle
466
Chapter 16 Wired
:
FOUR CHAR CODE('clik'), FOUR CHAR CODE('cend'), FOUR CHAR CODE('trig'), FOUR CHAR CODE('entr'), FOUR CHAR CODE('exit'), FOUR CHAR CODE('fram'), FOUR CHAR CODE('idle') m
= = = = = =
B
kQTEventType Event type
kAction 1 ~ kWhichAction 1
Action type
~ kActionParameter 1 Parameter data
Figure 16.6
The structureof an eventatom. Five of these actions concern various states of the cursor and the m o u s e button. W h e n the cursor moves over any n o n t r a n s p a r e n t part of a sprite's image, the kOTEventMouseEnter event is issued (whether or not the m o u s e button is down}, and w h e n the cursor then moves out of the sprite, the kOXEventMouseExit event is issued. W h e n the m o u s e button is clicked while the cursor is over a sprite, the kOTEventMouseC] i ck action is issued, and w h e n the mouse button is later released, the kOTEventMouseC1 i ckEnd event is issued. Note that the kQTEventMouseC] i ckEnd event is issued w h e t h e r or not the cursor is still inside of the sprite. If we w a n t to m a k e a w i r e d sprite act like a button, then we can use the kOTEventMouseClickEndTriggerButton event, w h i c h is issued only if the mouse button is clicked and released inside of the sprite's image. [This should r e m i n d you of the behavior of standard user interface buttons, w h i c h are not triggered if the m o u s e button is released after the cursor is moved outside of the button.} W h e n I say here that one of these events is issued, I m e a n that the sprite media handler looks for an event atom of that type inside of the sprite atom; if it finds one, then it interprets and executes the action atoms inside of that event atom. Only the action atoms associated with the affected sprite are executed, however. So, for instance, if two different sprites have actions associated with an event of type kQTEventMouseC1 i ck, then only the actions in the event atom of the sprite actually clicked are executed. If two sprites happen to overlap, the event is sent to the topmost sprite {the one w i t h the lower layer property}. The kQTEventFrameLoaded event is issued w h e n a sprite track key frame is loaded. It is not sent to any particular sprite, and hence the event atom that holds the associated actions is not contained inside of any sprite atom.
Events, Parameters,Actions 467
Rather, the event atom is stored in the media sample atom container, as a sibling of the sprite atoms. Frame-loaded events are handy for initializing the state of the sprites in the frame. (More on this later.) Finally, the kOTEventIdl e event is sent to every sprite in a sprite track periodically, after some predefined n u m b e r of ticks (that is, sixtieths of a second) has elapsed. The frequency with which these idle events are issued is determined by the kSpriteTrackPropertyOTIdleEventsFrequency property of the sprite track. This event is especially useful for triggering actions after some a m o u n t of time has elapsed; it will be very useful to us, for instance, w h e n we build our draggable sprite movie. Subsequent versions of QuickTime have added several more event types, but we will not consider them in this chapter. The most interesting one, perhaps, is the event tlcpe kQTEventKey, which is issued w h e n keys on the keyboard are pressed. In Chapter 18, "Back In Action," we'll play with some of these additional event types.
Specifying Actions OK, so now we know how to get a sprite to stand up and take notice w h e n certain kinds of events occur: we put an event atom with the appropriate atom ID into the sprite atom (as a sibling of the sprite property atoms that are inside the sprite atom). And we know what happens next: all the action atoms contained inside that event atom are interpreted and executed. So all we really need to learn is what kinds of actions can be triggered by QuickTime events; that is, we need to learn which values we can use as the data in an atom of type kWhichAction (see Figure 16.6 again). Here comes the really good news: the latest Rovies.h file contains over 100 constants that we can use to specify action types. There are action types for setting a movie's volume, setting a sprite's image index, setting a track's graphics mode, and opening a URL in the user's default browser, and on and on. And, every one of them can be wired to any one of the available QuickTime events. So we could set up a sprite track in which dragging a certain sprite from right to left changes the balance of the movie's sound track, or moving the mouse over a certain sprite changes the current pan angle of a QuickTime VR node. Really, the sky's the limit. The available actions are best categorized by the target of the action (that is, the object that is affected by the action). QuickTime currently supports three kinds of targets: movies, tracks, and sprites. There are also some actions that have no target. A large n u m b e r of actions perform operations on a movie. By' default, the target of these actions is the current movie (the one that contains the sprite track), but it's also possible to target other movies. Here's a representative sample of actions that operate on movies:
461] Chapter 16 Wired
enum { kAct i onMovi eSet Volume
= 1024,
kActi onMovi eSetRate kAct i onMovi eSet Loopi ngFl ags kAct i onMovi eGoToTime kAct i onMov i eGoToTi meByName
= = = =
1025, 1026, 1027, 1028,
kActi onMovi eGoToBegi nni ng kAct i onMovi eGoToEnd kActi onMovi eStepForward kActi onMovi eStepBackward
= = = =
1029, 1030, 1031, 1032,
kAct i onMovi eSetSel ect i on
= 1033
A n u m b e r of actions operate on tracks within a movie. The default track is the one that contains the sprite that triggers the action. Here are all the currently defined actions that operate on tracks: enum { kAct i onTrac kSet VoI ume kAct i onTrackSetBal ance kAct i onTrackSet Enabled kActi onTrackSetMatri x kAct i onTrac kSet Layer kActionTrackSetCl ip kAct i onTrac kSetCursor kAct i onTrac kSetGraph i csMode
= 2048, = 2049, = = = =
2050, 2051, 2052, 2053,
= 2054, = 2055
Of course, some of these actions make no sense if applied to a sprite track (for instance, kActionTrackSetVolume). I n those cases, w e need to s p e c i f y
some other track as the target of the action. We'll see how to do that in the next chapter. A n u m b e r of actions operate on sprites in a sprite track. The default sprite is the one that contains the event atom that triggers the action. H e r e ' s a handful of actions that operate on sprites: enum { kActi onSpri teSetMatri x kAct i onSpri teSet Imagel ndex kActi onSpri teSetVi si bl e kAct i onSpri teSet Layer kActi onSpri teSetGraphi csMode
= 3072, = 3073, = 3074, = 3075, = 3076
Events, Parameters, Actions 469
Finally, a fair number of actions have no target at all. A good example here is kActionGoToURL, which opens the user's default Web browser and loads a URL.
Specifying Parameters How does the kActionGoToURL action know which URL to open? The URL is specified in a parameter atom that is a child of the kAction atom. The number of parameters required by each action and the type of data in those parameters is detailed in Apple's technical documentation on wired sprites; it's also listed in Movies. h, for example, like this: kActionGoToURL
= 6146, /* (C string urlLink) */
This tells us that the kActionParameter atom in the action atom must contain a NULL-terminated string of characters that specifies the URL to open. Other actions take more than one parameter. For instance, the kActionMovieSetSelection action requires two parameters, a TimeValue that indicates the beginning time of the selection and another that indicates the ending time of the selection. In this case, the action atom must contain two atoms of type kActionParameter, one with atom index 1 and the other with atom index 2.
Controlling MovJes Using Wired Sprites That's enough theory for the moment. Let's get started wiring some sprites, and let's begin by seeing how to build the sprite track shown in Figure 16.4, which allows the user to control a movie by clicking the sprite "buttons." There are two main steps involved here: first we need to build the sprite track {which contains the sprite images, sprites, and initial sprite properties}, and then we need to wire those sprites with actions.
Building the Sprite Track We're already pretty adept at building sprite tracks, so we can proceed quickly here. In the present case, we're going to add a sprite track to an existing movie, so we'll start by getting some information about that movie, including its duration and size:
470
Chapter 16 Wired
GetMovieBox(theMovie, &myRect) ; myWidth = Long2Fix(myRect.right - myRect.left) ; myHeight = Long2Fix(myRect.bottom - myRect.top) ; myDuration = GetMovieDuration(theMovie) ; myTimeScale = GetMovieTimeScale(theMovie) ;
We'll use these values to set the size {and later the duration) of our new sprite track, like this: myTrack = NewMovieTrack(theMovie, myWidth, myHeight, kNoVolume); myMedia = NewTrackMedia(myTrack, SpriteMediaType, myTimeScale, NULL, 0);
Now, of course, we need to add some sample data to the new sprite track. As you know, this data includes the sprite images and initial sprite properties. At this point, the QTWired_AddSpriteControllerTrack function calls another QTWiredSpritesJr function, QTWired_AddControllerButtonSamplesToMedi a, to add the appropriate media data: QTWired AddContro 11erBut tonSampI es ToMedi a (myMedi a, myRect.right - myRect.left, myRect.bottom - myRect.top, myDuration) ;
Let's pause and reflect for a moment. We want the sprites in our sprite track to act like buttons, so that clicking one of the sprites triggers some appropriate action. We also want the sprite image to change while the mouse button is held down over the sprite, in just the same way that a standard button changes its appearance w h e n clicked. Figure 16.7 shows the movie w h e n the user clicks the Play sprite and holds the mouse button down. So really we need two images for each sprite, one to be displayed w h e n the sprite is in the normal state and one to be displayed w h e n the sprite is in the "pressed" state. Figure 16.8 shows the 12 images that we are going to add to the sprite track. If we store these images in consecutively n u m b e r e d 'PICT' resources, we can add them all to the sprite key frame sample with these lines of code: for (myCount = O; myCount < kNumControllerlmages; myCount++) SpriteUti I s AddPICTImageToKeyFrameSample(mySample, kFirstControllerImageID + myCount, &myKeyColor, myCount + 1, NULL, NULL); m
Now let's set the initial properties of the six sprites. For the Go-To-Start sprite (the first button on the left}, we can use the code in Listing 16.1.
Controlling Movies Using Wired Sprites 471
Figure 16.7 The button-down image for the Play sprite.
Figure 16.8 The sprite images.
Listing 16.1 Setting the properties of the Go-To-Start sprite. mySixth = theTrackWidth / kNumControllerButtons; myErr = QTNewAtomContainer(&myStartButton) ; i f (myErr ! = noErr) goto bai I ; myLocation, h myLocati on. v isVisible mylndex myLayer
= (kToBeginSpritePosition * mySixth) + ((mySixth - kButtonWidth) / 2); = theTrackHeight - (kButtonHeight + 5); = true; = kToBeginUplndex; =1;
SpriteUtils SetSpriteData(myStartButton, &myLocation, &isVisible, &myLayer, &mylndex, NULL, NULL, NULL); SpriteUtils AddSpriteToSample(mySample, myStartButton, kToBeginSpriteAtomID) ; D
472
Chapter 16 Wired
The only mildly tricky thing here is the code we use to determine the horizontal position of the sprite. We first calculate one-sixth of the track w i d t h and then move over the desired n u m b e r of sixths; then we back up just enough that the sprite image is nicely centered in the appropriate sixth of the sprite track. If we repeat the steps in Listing 16.1 for the remaining five sprites, add the key frame sample to the media, and add the media to the track, the original movie will then have a new sprite track with the sprites laid out as shown in Figure 16.4. Of course, clicking on the sprites w o n ' t have any effect, since we haven't yet attached any actions to those sprites. Let's see how to do that.
W i r i n g Actions to Sprites To wire some actions to a sprite, we need to add one or more atoms of type kQTEventType to the sprite atom and then add one or more action atoms to each of those event atoms. In the case of the Go-To-Start sprite, we want to add three event atoms, each of which contains a single action atom: 1. W h e n the mouse button is clicked and the cursor is over the sprite, we want to change the sprite's image index property so that the "pressed" image is the current image. That is, we want to change the image to kToBegi nDownIndex.
2. W h e n the mouse button is released (whether or not the cursor is still within the sprite), we want to change the sprite's image index property so that the normal image is once again the current image. That is, we want to change the image back to kToBeginUpIndex. 3. W h e n the mouse button is released and the cursor is still within the sprite, we want to execute the kActionMovieGoToBeginning action. Listing 16.2 shows some code that we can use to handle mouse clicks on the Go-To-Start sprite.
Listing 16.2 Handling mouse clicks on the Go-To-Start sprite. myErr = QTlnsertChild(myStartButton, kParentAtomlsContainer, kQTEventType, kQTEventMouseClick, I, O, NULL, &myEventAtom) ; i f (myErr ! = noErr) goto bai I ;
Controlling Movies Using Wired Sprites 4 7 3
/ / add an action atom to the event handler myErr = QTInsertChild(myStartButton, myEventAtom, kAction, O, O, O, NULL, &myActionAtom); i f (myErr l = noErr) goto bai I ; myAction = EndianU32 NtoB(kActionSpriteSetlmagelndex) ; myErr = QTInsertChild(myStartButton, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); i f (myErr ! = noErr) goto bai I ; / / add a parameter to the set image index action: image index mylndex = EndianU32 NtoB(kToBeginDownlndex); myErr = QTInsertChild(myStartButton, myActionAtom, kActionParameter, O, (short)kFirstParam, sizeof(mylndex), &mylndex, NULL); E
As you can see, we add an event atom whose atom ID is kQTEventMouseCl i ck to the sprite atom (myStartButton). Then we insert an action atom (of type kAction) into that event atom. The action atom is given t~vo children, one of type kWhichAction whose atom data is kActionSpriteSetlmagelndex, and one of type kActionParameter whose atom data is kToBeginDownlndex. In a nutshell, this says: w h e n the user clicks the Go-To-Start sprite, change that sprite's image index to kToBeginDownIndex. Listing 16.3 shows some code that we can use to handle mouse-up events for the Go-To-Start sprite. The code is entirely analogous to the code in Listing 16.2; all that has changed is the ID of the event atom and the index of the image.
Listing 16.3 Handling mouse-up events for the Go-To-Start sprite. myErr = QTInsertChild(myStartButton, kParentAtomlsContainer, kQTEventType, kQTEventMouseClickEnd, 1, O, NULL, &myEventAtom); i f (myErr l= noErr) goto bai I ; / / add an action atom to the event handler myErr = QTInsertChild(myStartButton, myEventAtom, kAction, O, O, O, NULL, &myActionAtom); i f (myErr ! = noErr) goto bai I ; myAction : EndianU32 NtoB(kActionSpriteSetlmagelndex) ; myErr = QTInsertChild(myStartButton, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); i f (myErr ! = noErr) goto bai I ; D
474
Chapter 16 Wired
/ / add a parameter to the set image index action: image index mylndex = EndianU32 NtoB(kToBeginUpIndex); myErr = QTInsertChild(myStartButton, myActionAtom, kActionParameter, O, (short)kFirstParam, sizeof(mylndex), &myIndex, NULL); D
It's actually even easier to wire the kActionMovieGoToBeginning action to the kQTEventMouseCl i ckEndTriggerButton event, since there are no parameters. Listing 16.4 shows how we handle this.
Listing 16.4 Handling button-trigger events for the Go-To-Start sprite. myErr = QTInsertChild(myStartButton, kParentAtomlsContainer, kQTEventType, kQTEventMouseClickEndTriggerButton, 1, O, NULL, &myEventAtom); i f (myErr ! = noErr) goto bai I ; / / add an action atom to the event handler myErr = QTInsertChild(myStartButton, myEventAtom, kAction, O, O, O, NULL, &myActionAtom); i f (myErr l= noErr) goto bai I ; myAction = EndianU32 NtoB(kActionMovieGoToBeginning); myErr = QTInsertChild(myStartButton, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL);
So adding simple wired actions to sprites really is just another exercise in constructing atom containers and atoms. Figure 16.9 shows the structure of the Go-To-Start sprite atom. (Note that several of the sprite property atoms have been omitted.)
Setting the Track Properties We still have a couple of things we need to do before our movie is fully wired. For one thing, we need to indicate to the movie controller that the sprite track has actions in it. We do this by including in the track's media property atom an atom of type kSpriteTrackPropertyHasActions whose atom data is set to true. (For more discussion of media property atoms, see Chapter 14, "A Goofy Movie.") Listing 16.5 shows our definition of QTWired_SetTrackVroperties, which is similar to the QTSprites SetTrackProperties function we used in previous chapters.
Controlling Movies Using Wired Sprites 475
kSpri teAtomType kToBeginSpriteAtomlD
I
I
kSpri tePropertyImagelndex 1 kToBegi nUpIndex kQTEventType kQTEventMouseCli ck kAction 1
kQTEventType kQTEventMouseClickEnd
I
kAction i
I kWhichAction 1 kActi onSpriteSetImagelndex I kActionParameter 1 kToBegi nDownIndex
kSpri tePropertyVi si bl e 1 1 kQTEventType kQTEventMouseCli ckEndTriggerButton
I
kWhichAction 1
I
Xi
kActionSpriteSetlmagelnde
kAction 1
I
kWhichActi on 1 kAct i onMovi eGoToBegi nni ng
I kActionParameter 1 kToBeginUpIndex
Figure 16.9 The Go-To-Start sprite atom. Listing 16.5 Setting the sprite track properties. void QTWired_SetTrackProperties (Media theMedia, UInt32 theldleFrequency) QTAtomContainer RGBCoI or Boolean UInt32 OSErr
myTrackProperties; myBackgroundCoI or; hasAct ions; myFrequency; myErr = noErr;
/ / add a background color to the sprite track myBackgroundColor.red = EndianU16 NtoB(Oxffff) ; myBackgroundColor.green = EndianU16 NtoB(Oxffff) ; myBackgroundColor.blue = EndianU16 NtoB(Oxffff) ; m
myErr : QTNewAtomContainer(&myTrackProperti es) ; i f (myErr == noErr) { QTInsertChild(myTrackProperties, O, kSpriteTrackPropertyBackgroundColor, i, i, sizeof(myBackgroundColor), &myBackgroundColor, NULL);
476
Chapter 16 Wired
/ / tell the movie controller that this sprite track has actions hasActions = true; QTInsertChild(myTrackProperties, O, kSpriteTrackPropertyHasActions, 1, 1, sizeof(hasActions), &hasActions, NULL); / / tell the sprite track to generate QTIdleEvents myFrequency = EndianU32 NtoB(theldleFrequency) ; QTInsertChild(myTrackProperties, O, kSpriteTrackPropertyQTIdleEventsFrequency, 1, 1, sizeof(myFrequency), &myFrequency, NULL); SetMedi aPropertyAtom(theMedi a, myTrackProperti es) ; QTDisposeAtomContai ner (myTrackProperti es) ;
You'll notice that QTWired_SetTrackProperties also adds an atom of type kSpriteTrackPropertyQTIdleEventsFrequency to the media property atom. As we learned earlier, this atom specifies the n u m b e r of ticks between idle events. Our current sprite track doesn't use idle events, so we pass the parameter kNoQTIdleEvents w h e n we call QTWired_SetTrackProperties, which tells the movie controller not to issue idle events at all. Another useful value is 0, which tells the movie controller to issue idle events as fast as possible.
Setting the Track Layer Each track in a QuickTime movie has a layer, which determines the order in which the track is drawn into the movie. Tracks with lower layers are d r a w n after tracks with higher layers, so they will be drawn on top of those higherlayered tracks. W h e n a track is created, its layer is set to 0. In the current case, we want to ensure that our new sprite track is drawn on top of all existing tracks in the movie, since otherwise it might be hidden behind those tracks (and hence be pretty much useless). So we need to get the lowest track layer of those existing tracks and then set the sprite track's layer to some lower value. QTWiredSpritesJr does that with these lines of code: SetTrackLayer(myTrack, kMaxLayerNumber); SetTrackLayer(myTrack, QTWired GetLowestLayerlnMovie(theMovie) - I) ;
A layer is a value of type short, so the largest possible layer value is 0xTfff: #define kMaxLayerNumber
Ox7fff
Controlling Movies Using Wired Sprites 477
Listing 16.6 shows our definition of the QTWired_GetLowestLayerlnMovie function.
Listing 16.6 Finding the lowest track layer in a movie. short QTWired GetLowestLayerlnMovie (Movie theMovie)
{
m
long long short short
myCount = O; mylndex; myLayer = O; myMinLayer= kMaxLayerNumber;
myCount = GetMovieTrackCount(theMovie) ; for (myIndex = 1; mylndex <= myCount; mylndex++) { myLayer = GetTrackLayer(GetMovielndTrack(theMovie, mylndex)) ; i f (myLayer < myMinLayer) myMinLayer = myLayer;
}
return (myMinLayer) ;
Setting the Movie Controller A wired sprite movie must be associated with a movie controller at playback time. It's the movie controller's job to determine whether a sprite track contains an}, event atoms and then to monitor the mouse and keyboard for any relevant events. When one of the designated events occur, the movie controller notifies the appropriate sprites. This of course is not a problem for us. All of our sample applications (except for one) use movie controllers to manage the user's interactions with a movie. We've seen, sufficiently I hope, that using movie controllers can buy us a tremendous amount of functionality. There is, however, one troubling issue that arises here. By default, the standard movie controller displays the controller bar [usually along the bottom edge of the movie). But in some cases we might not want the controller bar to be displayed. This is especially true in the case of the movie that we just built; after all, what's the point of devising our own sprite buttons for controlling a movie if we're saddled with the standard controller bar? To address this issue, QuickTime 3.0 introduced a new type of movie controller called the no-interface movie controller (or sometimes the none movie controller). The no-interface movie controller operates just like the standard movie controller except that no controller bar is displayed and no keyboard
478
Chapter 16 Wired
events are passed to it. (Also, we cannot display a badge on a movie that's associated with the no-interface movie controller, but that's sort of a corollary of the fact that no controller bar is displayed.) The no-interface movie controller is made to order for movies in w h i c h we use wired sprites to handle user interaction with the movie or with its elements. But how do we tell QuickTime to use the no-interface movie controller for some particular movie? We do this by attaching a user data item of type kUserDataMovieControl lerType to the movie, whose item data is FOUR_ CHAR CODE( 'none' ), like this: OSType myType = FOUR_CHAR_CODE('none'); myType = EndianU32 NtoB(myType); SetUserDatal tem(GetMovi eUserData (theMovi e), &myType, si zeof (myType), kUserDataMovieControllerType, 1) ;
When an application calls NewMovieControl l er to attach a movie controller to a movie, NewMovieController first looks for a movie user data item of type kUserDataMovieControl lerType and, if it finds one, tries to open an instance of a movie controller component having the subtype specified by that item's data. This occurs transparently to the application (which is w h y our existing applications are able to play wired sprite movies using the no-interface movie controller).
Setting the Track Matrix It's time for a small but important digression. Recall that we determined the size of the new sprite track by calling fietMovi eBox, like this: GetMovieBox(theMovie, &myRect) ; myWidth = Long2Fix(myRect.right - myRect.left) ; myHeight = Long2Fix(myRect.bottom - myRect.top) ;
This method works in almost all cases--I'd venture to guess that it will w o r k for at least 98% of all existing QuickTime movies. But in a few cases, it fails miserably. Figure 16.10 shows what happens if we try to add our sprite button track to the toy train movie we encountered in an earlier chapter. To understand what's causing this problem and how we can avoid it, we need to delve a bit deeper into a movie's geometry. W h e n a track's media handler draws some data, it draws it into the rectangle (called the track rectangle) defined by the track's dimensions. The coordinate system in which the point (0, 0) is at the upper-left corner of the track rectangle defines the track coordinate system. When a track is composited into a movie, the track data is transformed using a track matrix, which maps the track coordinate
Controlling Movies Using Wired Sprites 479
Figure 16.10 The toy train movie after adding the sprite button track.
system into the movie coordinate system. But before the movie data is displayed on the output device, that data is transformed once again. QuickTime applies the movie matrix, which maps the movie coordinate system into the display coordinate system (namely, the QuickDraw global coordinate plane). In most cases, the track matrices and the movie matrix are the identity matrix, so we can safely ignore these various transformations. But a movie can have a non-identity movie matrix. The toy train movie is one such movie, and that's what causes the problem seen in Figure 16.10. The GetMovieBox function gives us the size and location of the movie in display coordinates, after the track and movie matrices have been applied. Using the movie box to determine the dimensions of the sprite track is guaranteed to produce the wrong results if the movie has a non-identity movie matrix, since the track rectangle is always transformed using that matrix before being drawn to the screen. A solution to this problem is actually fairly simple. All we need to do is compensate for the movie matrix by calculating the inverse of the movie matrix and installing it as the sprite track's track matrix, like this: GetMovieMatrix(theMovie, &myMatrix) ; i f (InverseMatrix(&myMatrix, &mylnverseMatrix)) SetTrackMatrix(myTrack, &mylnverseMatrix) ;
The InverseMatrix function returns, in the second parameter, the inverse of the matrix specified by the first parameter. If it succeeds in inverting the first matrix, it returns true (and fal se otherwise). So the sprite track's track
480
Chapter 16 Wired
matrix and the movie matrix essentially cancel each other out, which is sufficient to place the sprite track in the right place. One moral of this story is that it's good to test our code on a wide variety of movies to make sure that it's able to handle pretty much anything the user might throw at it. It's perfectly legal--albeit rare--for a movie to have a non-identity movie matrix. So our applications should be smart enough to handle them correctly.
Putting It All Together Listing 16.7 shows our complete definition of the QTWired_AddSpriteControlI erTrack function.
Listing 16.7 Adding a sprite controller track to a movie. OSErr QTWired AddSpriteControllerTrack (Movie theMovie) Track Media Matri xRecord RGBColor Fixed Rect TimeValue TimeValue OSType OSErr
myTrack = NULL; myMedia = NULL; myMatri x; myKeyColor; myWidth, myHeight; myRect; myDuration = OL; myTimeScale = OL; myType = FOUR CHAR CODE('none') ; myErr = noErr; m
i f (theMovie == NULL) { myErr = paramErr; goto bai I ;
}
/ / get some information about the target movie GetMovieBox(theMovie, &myRect) ; myWidth = Long2Fix(myRect.right - myRect.left); myHeight = Long2Fix(myRect.bottom - myRect.top) ; myDuration = GetMovieDuration(theMovie) ; myTimeScale = GetMovieTimeScale(theMovie) ; / / create a new sprite track in the target movie myTrack = NewMovieTrack(theMovie, myWidth, myHeight, 255); myMedia = NewTrackMedia(myTrack, SpriteMediaType, myTimeScale, NULL, 0);
Controlling Movies Using Wired Sprites 481
/ / set the track matrix to compensate for any existing movie matrix GetMovieMatrix(theMovie, &myMatrix) ; i f (InverseMatrix(&myMatrix, &myInverseMatrix)) SetTrackMatrix(myTrack, &mylnverseMatrix) ; myErr = BeginMediaEdits(myMedia) ; i f (myErr != noErr) goto bai I ; / / add sprite images and sprites to the sprite track; add actions to the sprites QTWired AddControl I erButtonSampl esToMedia (myMedia, myRect.right - myRect.left, myRect.bottom - myRect.top, myDuration); myErr = EndMediaEdi ts (myMedia) ; i f (myErr != noErr) goto bai I ; / / add the media to the track InsertMedialntoTrack(myTrack, O, O, GetMediaDuration(myMedia), fixed1); / / set the sprite track properties QTWired SetTrackProperties(myMedia, kNoQTIdleEvents) ; myKeyColor.red = myKeyColor.green = myKeyColor.blue = Oxffff; / / white MediaSetGraphi csMode(GetMediaHandl er(myMedia), transparent, &myKeyColor) ; / / make sure that the sprite track is in the frontmost layer SetTrackLayer(myTrack, kMaxLayerNumber); SetTrackLayer(myTrack, QTWired GetLowestLayerlnMovie(theMovi e) - I) ; / / select the "no-interface" movie controller myType = EndianU32 NtoB(myType); SetUserDataltem(GetMovieUserData(theMovie), &myType, sizeof(myType), kUserDataMovieControl lerType, 1) ; m
bail: return (myErr) ;
}
Wired Sprite Utilities You'll notice that I didn't include the complete listing of the QTWired_AddControl l erButtonSampl esToMedia function. The reason for this should be clear
from a brief glance at Listings 16.2 through 16.4: adding simple button behaviors to all of the six sprites would require a total of about 200 lines of
4.82
Chapter 16 Wired
code. Obviously, for both readability and maintainability, it would behoove us to encapsulate parts of that function into some reusable utility functions, in just the same w a y that we previously facilitated our w o r k with sprites by using the functions in S p r i t e U t i l i t i e s . c . For the most c o m m o n operations with wired sprites, we can use the functions in the file WiredSpriteUtilit i es. c. In this section, w e ' r e going to consider a few of those functions.
Adding Event and Action A t o m s In Listings 16.2 through 16.4, we begin by adding an event atom of the appropriate type to the existing sprite atom. At that point, since w e ' v e just created the sprite atom ourselves, we know that it doesn't already have any event atoms in it, so we can safely just call QTInsertChi l d to add one. More generally however, it would be better to look and see w h e t h e r a sprite atom already contains an event atom of the type we want to add. If it does, then we can just add our new action atoms to that event atom. If it doesn't, then we need to create a new atom of that type. Listing 16.8 shows the definition of the utility function WiredUtil s_AddOTEventAtom, which returns to us either the existing event atom or a new event atom. Listing 16.8 Adding an event atom. OSErr Wi redUti I s_AddQTEventAtom (QTAtomContainer theContai ner, QTAtom theActi onAtoms, QTAtomID theQTEventType, QTAtom *theNewQTEventAtom)
(
OSErr
myErr = noErr;
i f ((theContainer == NULL)II (theQTEventType == O ) I I myErr = paramErr; goto bai I ;
(theNewQTEventAtom == NULL)) {
}
i f (theQTEventType =: kQTEventFrameLoaded) { *theNewQTEventAtom = QTFindChildByID(theContainer, theActionAtoms, kQTEventFrameLoaded, 1, NULL); i f (*theNewQTEventAtom == O) myErr = QTInsertChild(theContainer, theActionAtoms, kQTEventFrameLoaded, I, 1, O, NULL, theNewQTEventAtom); } else { *theNewQTEventAtom = QTFindChildByID(theContainer, theActionAtoms, kQTEventType, theQTEventType, NULL); i f (*theNewQTEventAtom == O) myErr = QTInsertChild(theContainer, theActionAtoms, kQTEventType, theQTEventType, 1, O, NULL, theNewQTEventAtom);
Wired Sprite Utilities
483
bail: return (myErr) ;
}
Notice that frame-loaded events are treated differently here. The a t o m type of a frame-loaded event is kOTEventFrameLoaded (not kOTEventType, as w i t h other events), and the ID should be 1. Also, for frame-loaded events, we expect the caller to set the theActionAtoms p a r a m e t e r to kParentAtomIsContai ner so that the event is inserted as a child of the container atom. Once we have an event atom, we'll w a n t to add one or m o r e action a t o m s to it. As w e know, an action a t o m is a p a r e n t atom that always includes a child of type kWhichAction. We can use the WiredUtils_AddActionAtom function, defined in Listing 16.9, to add an action atom to an event atom.
Listing
16.9
Adding an action atom to an event atom.
OSErr WiredUtils AddActionAtom (QTAtomContainer theContainer, QTAtom theEventAtom, long theActionConstant, QTAtom *theNewActionAtom)
(
QTAtom OSErr
myActionAtom= O; myErr = noErr;
i f ((theContainer --= NULL) II (theActionConstant == 0)) { myErr = paramErr; goto bai I ;
}
myErr = QTInsertChild(theContainer, theEventAtom, kAction, O, O, O, NULL, &myActionAtom); i f (myErr ! = noErr) goto bai I ; theActionConstant = EndianU32 NtoB(theActionConstant); myErr = QTInsertChild(theContainer, myActionAtom, kWhichAction, 1, 1, sizeof(theActionConstant), &theActionConstant, NULL); bail: i f (theNewActionAtom l= NULL) { i f (myErr != noErr) *theNewActionAtom = O; else *theNewActi onAtom = myActi onAtom;
}
return (myErr) ;
484
Chapter 16 Wired
The value w e pass for theEventAtom can be kParentAtomlsContainer, in w h i c h case the action a t o m is a d d e d as a child of theContainer. This will be useful later w h e n w e w a n t to create complex w i r e d actions involving conditional execution of actions. WiredSpri teUti 1 i t i es. c defines the function WiredUti 1s_AddQTEventAndActionAtoms (Listing 16.10), w h i c h just calls WiredUtils_AddQTEventAtom a n d t h e n WiredUt i 1s AddActi onAtom.
Listing 16.10 Adding event and action atoms.
OSErr WiredUtils_AddQTEventAndActionAtoms (QTAtomContainer theContainer, QTAtom theAtom, long theEvent, long theAction, QTAtom *theActionAtom)
{
QTAtom OSErr
myEventAtom= O; myErr = noErr;
myEventAtom = theAtom; i f (theEvent l= O) { myErr = WiredUtils_AddQTEventAtom(theContainer, theAtom, theEvent, &myEventAtom); i f (myErr ! = noErr) goto bai I ;
}
myErr = WiredUtils AddActionAtom(theContainer, myEventAtom, theAction, theActionAtom); m
bail: return (myErr) ;
}
To add a p a r a m e t e r to an action, we add a child a t o m of type kActionParameter. WiredSpriteUtilities.c defines the function WiredUtils AddActionParameterAtom (Listing 16.11), which just calls QTInsertChild. Bear in mind that the data passed in the theParamData parameter must be in bigendian format, since WiredUtil s_AddActionParameterAtom does not perform
any byte swapping. Listing 16.11 Adding an action parameter atom.
OSErr WiredUtils AddActionParameterAtom (QTAtomContainer theContainer, QTAtom theActionAtom, long theParameterlndex, long theParamDataSize, void *theParamData, QTAtom*theNewParamAtom) return(QTlnsertChild(theContainer, theActionAtom, kActionParameter, O, (short)theParameterlndex, theParamDataSize, theParamData, theNewParamAtom));
Wired Sprite Utilities 4 8 5
Setting a Sprite Image Index Let's put these utilities to work for us. Listing 16.12 defines the Wi redUtils_ AddSpriteSetlmagelndexAction function, which adds to the sprite atom theAtom the children necessary to change the sprite's image index in response to the event specified by the theEvent parameter. (For the moment, we'll ignore the call to WiredUti 1s_AddTrackAndSpriteTargetAtoms, since our sprite actions in this chapter all use the default targets.) Listing 16.12 Adding a sprite index setting action atom. OSErr WiredUti I s AddSpriteSetlmagelndexAction (QTAtomContainer theContainer, QTAtom theAtom, long theEvent, long theTrackTargetType, void *theTrackTarget, long theTrackTypeIndex, long theSpriteTargetType, void *theSpriteTarget, short theImagelndex, QTAtom *theActionAtom) QTAtom OSErr
myActionAtom= O; myErr = noErr;
myErr = WiredUt i I s_AddQTEventAndActi onAtoms (theContai ner, theAtom, theEvent, kActionSpriteSetImagelndex, &myActionAtom) ; i f (myErr i= noErr) goto bai I ; thelmagelndex = EndianS16 NtoB(thelmagelndex); myErr = Wi redUti I s AddActi onParameterAtom(theContai ner, myActi onAtom, kFi rstParam, sizeof(thelmagelndex), &thelmagelndex, NULL); i f (myErr I= noErr) goto bai I ; myErr = WiredUti I s AddTrackAndSpriteTargetAtoms (theContainer, myActionAtom, theTrackTargetType, theTrackTarget, theTrackTypelndex, theSpriteTargetType, theSpriteTarget) ; i f (theActionAtom l= NULL) *theActi onAtom = myActi onAtom; bail: return (myErr) ;
}
Now we can go back and rework some of our earlier code. In particular, all the code in Listings 16.2 through 16.4 can be replaced by these lines:
486
Chapter 16 Wired
WiredUt i I s_AddSpri teSet ImageIndexActi on (myStartButton, kParentAtomlsContainer, kQTEventMouseClick, O, NULL, O, O, NULL, kToBeginDownlndex, NULL); WiredUti I s AddSpriteSetlmagelndexAction (myStartButton, kParentAtomlsContainer, kQTEventMouseClickEnd, O, NULL, O, O, NULL, kToBeginUplndex, NULL); WiredUti I s AddMovieGoToBeginningAction(myStartButton, kParentAtomlsContainer, kQTEventMouseClickEndTriggerButton) ;
V a r i a b l e s and Conditionals In addition to the basic event-and-action interactivity that we've witnessed so far, QuickTime also supports what we might call programmable interactivity. That is to say, our wiring can make use of standard programming concepts like variables, function calls, and flow control logic. In our wired actions, we can set and get the values of variables, read movie and system characteristics, and control the execution of actions using "if-then" and "while" constructs. As you can imagine, this opens the door for some sophisticated and truly amazing wired actions. In practice, variables, e n v i r o n m e n t checking, and flow control are always used in combination; so let's roll out the theory in this section and defer actual examples of their use until the next section.
Setting Variables Each sprite track can have a set of variables--called sprite track variables-whose values can be set and read by wired actions. Variables are particularly useful for maintaining state information, such as w h e t h e r a particular button is down or w h e t h e r a sprite has reached a certain location in the movie. In QuickTime 3, the values of sprite track variables were always floatingpoint numbers; beginning in QuickTime 4, these values can be either floating-point n u m b e r s or NULL-terminated strings. We refer to a sprite track variable using a variable ID, which is of type QTAtomlD. (A QTAtomlD is declared as a signed long integer, of type long.) To set the value of a sprite track variable, we execute either the kActi onSpri teTrackSetVariable or the kActionSpriteTrackSetVariableToString action. Both these actions require two parameters, the ID of the variable to be set and the desired value of the variable (which is a floating-point n u m b e r or a string, respectively). Listing 16.13 defines a function that we can use to set the value of a variable to a specified floating-point number.
Variables and Conditionals 487
Listing 16.13 Setting a sprite track variable. OSErr WiredUtil s AddSpriteTrackSetVariableAction (QTAtomContainer theContainer, QTAtom theAtom, long theEvent, QTAtomID theVariableID, float theValue, long theTrackTargetType, void *theTrackTarget, long theTrackTypelndex)
{
QTAtom OSErr
myActionAtom= O; myErr = noErr;
myErr = WiredUti I s_AddQTEventAndActionAtoms (theContai ner, theAtom, theEvent, kActionSpri teTrackSetVari abl e, &myActionAtom) ; i f (myErr ! = noErr) goto bai I ; theVariableID = EndianU32 NtoB(theVariableID); myErr = WiredUti I s AddActionParameterAtom(theContai ner, myActionAtom, kFi rstParam, sizeof(theVariableID), &theVariableID, NULL); i f (myErr ! = noErr) goto bai I ; EndianUti]s Float NtoB(&theValue); myErr = WiredUti I s AddActionParameterAtom(theContai ner, myActionAtom, kSecondParam, sizeof(theValue), &theValue, NULL); myErr = Wi redUti I s AddTrackTargetAtom(theContainer, myActionAtom, theTrackTargetType, theTrackTarget, theTrackTypelndex); m
bail. return (myErr) ;
}
We can perform numeric operations on the values of floating-point variables by creating expression atoms (which we'll consider shortly). Also, we can concatenate the values of two string variables by executing the kActi onSpri teTrackConcatVari ab] es action; this action requires three parameters, the variable IDs of the two string variables to be concatenated, and the variable ID of the variable to which the result is to be assigned.
Controlling Action Processing QuickTime supports two basic mechanisms for controlling the flow of wired action execution, the kActionCase action and the kActionWhile action. The kActionWhile action takes a single parameter, of type kConditionalAtomType. This atom is a parent atom that contains two children, one of type kExpressionContainerAtomType and another of type kActionListAtomType. The expres-
488
Chapter 16 Wired
sion container atom defines an expression; as long as the expression evaluates to a nonzero value, the action list is executed. {An action list is simply a list of one or more action atoms.) Figure 16.11 shows the general structure of a kActionWhi l e atom. A case atom is slightly more complex than a while atom. In a case atom, there is exactly one p a r a m e t e r atom, w h i c h contains an arbitrary n u m b e r of kConditionalAtomType atoms (which m a y have any u n i q u e atom IDs). W h e n a case atom is executed, the conditional atoms are evaluated (starting w i t h the atom with lowest index); w h e n the expression in one of the expression atoms evaluates to a nonzero value, the associated action atom list is executed. Figure 16.12 shows the general structure of a kActionCase atom. A case atom is analogous to an "if" statement followed by some n u m b e r of "else if" statements. We can therefore emulate an "if-then" construct by creating a case atom that contains just one conditional atom.
kAct i on i
kWh(chAct i on --
i
kActionWhi le kActi onParameter
kCondi t i onal AtomType
i
I
kExpres si onCont ai nerAt omType
kAct i onLi stAtomType
1
Expression data
Figure 16.11
The structure of a while atom.
t t
kAction m
Action data kAction n
Action data
Variables and Conditionals
489
kActi on 1
1
kWhchActi i on 1 kActionCase kActionParameter
I
kCondit i onalAtomType I kExpressionContainerAtomType 1 Expression data
I kActionListAtomType 1
Action list data
Figure 16.12
I
kCondi t i onalAtomType n I kExpressionContainerAtomType 1
Expression data
I kActionListAtomType I Action list data
The structure of a case atom.
Using Expressions and Operands Now we need to understand how to construct an expression container atom. An expression container atom is a parent atom that contains either an operator atom or an operand atom. Operator atoms provide a means of combining operand atoms {or, indeed, other operator atoms) using numerical and logical operations. For instance, Figure 16.13 shows the structure of an expression container atom that adds two operands together. Notice that the atom ID of an operator atom specifies the kind of operation to perform. Notice also that the IDs of the child operand atoms can be any unique IDs. For operations in which the order of the operands is important, they are ordered by the atom index. If an operator atom for a binary operation contains more than two children, the operation is applied to the first two children; then the operation is applied once more to that result and the third child, and so on until all the children have been used. This allows us to perform multiple operations without an undue amount of atom nesting.
490
Chapter16Wired
kExpressi onCont ai nerAt omType 1 kOperatorAtomType kOperatorAdd I
kOperandAtomType m Operand data
I kOperandAo tmType n Operand data
Figure 16.13 A sample expression container atom.
Operands are the "raw materials" for operations and expressions. There are two basic kinds of operands (aside from expressions, which can also serve as operands): constant operands and function operands. A constant operand is a leaf atom of type kOperandConstant whose atom data is a floatingpoint number; a constant operand atom is contained in an atom of type
kOperandAtomType.
A function operand returns information about some object, most often the current setting of some property of the operand's target. For instance, the k0perandMovieVo]ume operand returns the current volume of the target movie (which by default is the movie that contains the sprite track). And the k0perandSpriteTrackVariable operand returns the current value of a specified sprite track variable. And the k0perandMouseLocalHLoc operand returns the current horizontal position of the cursor. Most function operands take no parameters; when a parameter is required, it's added as a child of the operand atom (in just the same way that parameters are added to action atoms). The file Movies.h defines constants for over a hundred function operands. We can use them to get information about the user's Internet connection speed, the current day of the week, the current operating system, the movie rate, a track's width and height, a sprite's current image index, and so forth.
Draggable Sprites Let's now consider a real-life example that uses programmable actions, the draggable sprite movie shown in Figures 16.1 through 16.3. The basic idea is very simple: we want to set the position of the sprite to the position of the
Draggable Sprites 491
cursor for as long as the m o u s e b u t t o n is held d o w n over the sprite. T h e r e is (as of Q u i c k T i m e 6.0) no operand that r e t u r n s the c u r r e n t state of the m o u s e button, so we'll have to keep track of that state ourselves, using a sprite track variable. We define the ID of that variable like this:
#define kMouseStateVariabl eID T h e n we need to install three event atoms in the icon sprite atom" 1. W h e n the m o u s e button is clicked w i t h i n the sprite image, we w a n t to set the value of the mouse-state variable to 1. So we'll add a kActionSpriteTrackSetVariable action to the sprite's kQTEventMouseClick event atom. 2. W h e n the m o u s e button is released, we w a n t to set the value of the mouse-state variable to 0. So we'll add a kActionSpriteTrackSetVariable action to the sprite's kQTEventMouseCl i ckEnd event atom. 3. W h e n e v e r we receive an idle event, we'll check to see w h e t h e r the m o u s e button is d o w n or up; if it's down, we set the position of the sprite to the c u r r e n t position of the cursor. So w e ' l l add a c o n d i t i o n a l action to the sprite's kQTEvent I dl e event atom. These first two items are easy to do, especially now that we have our wired sprite utilities at hand:
WiredUti I s_AddSpriteTrackSetVariableAction(theContainer, mySpriteAtom, kQTEventMouseClick, kMouseStateVariableID, 1, O, NULL, 0); WiredUti I s_AddSpriteTrackSetVariabl eAction(theContainer, mySpriteAtom, kQTEventMouseClickEnd, kMouseStateVariableID, O, O, NULL, 0); The third item is a bit trickier, however, since it involves constructing a conditional action and using operands to read the current m o u s e position. Let's step through this process carefully. For the r e m a i n d e r of this section, we'll dispense with our standard error checking {solely to save some space). In addition, for the m o m e n t we'll also dispense with the w i r e d sprite utilities, so that we see how to do this using just the basic Q u i c k T i m e APIs.
Creating a Condition Suppose that myEventAtom is the idle event atom inside the icon sprite atom. We w a n t to add an "if-then" decision to this atom, so we begin by inserting an action atom of type kActi onCase together with a p a r a m e t e r atom that will serve as the parent of the conditional atom, w h i c h in turn is the parent of the expression container and atom list atoms (see Figure 16.12 again}:
492
Chapter 16 Wired
QTInsertChild(theContainer, myEventAtom, kAction, O, O, O, NULL, &myActi onAtom) ; myAction = EndianU32 NtoB(kActionCase); QTInsertChild(theContainer, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); QTInsertChild(theContainer, myActionAtom, kActionParameter, 1, kFirstParam, O, NULL, &myParamAtom); QTInsertChild(theContainer, myParamAtom, kConditionaIAtomType, O, 1, O, NULL, &myConditionaIAtom); m
Specifying the Conditional Expression The conditional atom contains an expression container atom that indicates the condition under which the action list contained in the conditional atom is to be executed"
QTInsertChi Id(theContainer, myConditionaIAtom, kExpressionContainerAtomType, 1, 1, O, NULL, &myExpressionAtom); The expression container atom, in turn, contains an expression. In the present case, we want the expression to be: "if the value of the variable with ID kMouseStateVariableID is equal to 1." This gets decomposed into an operator atom of type kOperatorEqualTo that has two child atoms {one for each of the two operands):
QTInsertChild(theContainer, myExpressionAtom, kOperatorAtomType, kOperatorEquaITo, I, O, NULL, &myOperatorAtom) The first child of the operator atom contains the constant value 1. So we need to insert an operand atom into the operator atom:
QTInsertChild(theContainer, myOperatorAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); Then we need to insert an operand type atom of type kOperandConstant; in this case, we can insert the constant value as the atom data, like this: myConstantValue = 1; QTInsertChild(theContainer, myOperandAtom, kOperandConstant, 1, 1, O, NULL, &myOperandTypeAtom); EndianUti I s Float NtoB(&myConstantValue); QTSetAtomData(theContai ner, myOperandTypeAtom, si zeof (myConstantValue), &myConstantValue) ;
Draggable Sprites 493
Note that we need to make sure that the atom data (of type float) is in bigendian format. Finally, we need to insert a second operand atom into the operator atom. This time, however, the operand is not a constant value; rather, it's the value of our sprite track variable. So we insert into the operand atom an o p e r a n d type atom of type kOperandSpriteTrackVariable; this atom then holds a p a r a m e t e r atom that specifies which sprite track variable to use.
QTInsertChild(theContainer, myOperatorAtom, kOperandAtomType, O, 2, O, NULL, &myOperandAtom); QTI nsertChi I d (theContai ner, myOperandAtom, kOperandSpriteTrackVari abl e, 1, 1, O, NULL, &myOperandTypeAtom); myVariableID = EndianU32 NtoB(kMouseStateVariableID) ; QTInsertChild(theContainer, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); And so we are finished building the expression container atom.
Specifying the Conditional Actions Now we need to build the action list atom that will be executed if the expression container atom evaluates to a nonzero value. We begin by inserting an action list atom into the conditional atom; this atom will contain a single child atom, which is an action atom of type kActionSpriteTranslate.
QTInsertChi I d(theContainer, myConditionaIAtom, kActionLi stAtomType, 1, 1, O, NULL, &myActionListAtom); QTInsertChild(theContainer, myActionListAtom, kAction, O, O, O, NULL, &myActi onAtom) ; myAction = EndianU32 NtoB(kActionSpriteTranslate); QTInsertChild(theContainer, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); m
The kActionSpriteTranslate action requires three parameters, which are the horizontal and vertical positions to translate to, and a Boolean value that indicates w h e t h e r those values specify an absolute or relative translation. We add the first p a r a m e t e r like this:
QTlnsertChild(theContainer, myActionAtom, kActionParameter, O, (short)kFirstParam, O, NULL, &myParameterAtom); QTInsertChild(theContainer, myParameterAtom, kExpressionContainerAtomType, 1, 1, O, NULL, &myExpressionAtom);
494
Chapter 16 Wired
QTInsertChild(theContainer, myExpressionAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); QTInsertChild(theContainer, myOperandAtom, kOperandMouseLocaIHLoc, 1, 1, O, NULL, NULL); The first parameter is an expression container atom that contains a function operand, k0perandMouseLocalHLoc, which returns the current horizontal position of the mouse. The second parameter retrieves the current vertical position of the mouse in exactly the same way:
QTInsertChild(theContainer, myActionAtom, kActionParameter, O, (short)kSecondParam, O, NULL, &myParameterAtom); QTInsertChild(theContainer, myParameterAtom, kExpressionContainerAtomType, 1, 1, O, NULL, &myExpressionAtom); QTInsertChild(theContainer, myExpressionAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); QTInsertChild(theContainer, myOperandAtom, kOperandMouseLocaIVLoc, I, I, O, NULL, NULL); The operands kOperandMouseLocalHLoc and kOperandMouseLocalVLoc, like all nonstring operands, return a floating-point value (of type float). But the kActionSpriteTranslate action expects its first two parameters to be of type Fixed. It's important to know that QuickTime automatically converts the floating-point values into Fixed values before passing them to kActionSpriteTranslate. So we don't need to worry about converting the data types here. Finally, we add the third parameter like this:
mylsAbsol ute = true; QTInsertChild(theContainer, myActionAtom, kActionParameter, O, (short)kThirdParam, sizeof(mylsAbsolute), &mylsAbsolute, NULL); And so we are finished building the action list atom.
Putting It All Together Again Listing 16.14 shows a version of the QTWired_MakeSpriteDraggable function. Once again I've omitted most of the error checking to enhance the readability of the code. And this version uses some wired sprite utilities to create the expression container and atom list atoms. The file QTWiredSpritesJr.c contains a more complete version that has better error checking; in addition, that version contains both wired utility calls and basic QuickTime API calls. So you can see how it's done both ways.
Draggable Sprites 495
Listing 16.14 Making a sprite draggable. OSErr QTWired_MakeSpriteDraggable (QTAtomContainer theContainer, QTAtomID theID)
{
mySpriteAtom = O; myEventAtom = O; myActionAtom = O; myParamAtom = O; myConditionaIAtom, myExpressionAtom, myOperatorAtom, myActionLi stAtom, myParameterAtom; myOperandlndex; myVariabl eID; myConstantVal ue; mylsAbsol ute; myErr = noErr;
QTAtom QTAtom QTAtom QTAtom QTAtom short QTAtomID float Boolean OSErr
/ / find the sprite atom with the specified ID in the specified container mySpriteAtom = QTFindChildByID(theContainer, kParentAtomlsContainer, kSpriteAtomType, theID, NULL); i f (mySpriteAtom == O) { myErr = paramErr; goto bai I ;
}
/ / add a mouse click event handler WiredUt i I s_AddSpri teTrackSet Vari abl eAct i on (theConta i ner, mySpri teAtom, kQTEventMouseClick, kMouseStateVariableID, 1, O, NULL, 0); / / add a mouse click end event handler WiredUti I s_AddSpri teTrackSetVari abl eActi on (theContai ner, mySpri teAtom, kQTEventMouseClickEnd, kMouseStateVariableID, O, O, NULL, 0); / / add an idle event handler WiredUt i I s_AddQTEventAndActi onAtoms(theContai ner, mySpriteAtom, kQTEventI dl e, kActionCase, &myActionAtom) ; / / add a parameter atom to the kActionCase action atom; this w i l l serve as a parent to / / hold the expression and action atoms WiredUtil s AddActionParameterAtom(theContainer, myActionAtom, kFirstParam, O, NULL, &myParamAtom); m
WiredUt i I s AddCondit i onal Atom(theContai ner, myParamAtom, 1, &myCondit i onal Atom) ; WiredUt i I s_AddExpressi onContai nerAtomType(theContai ner, myCondit i onal Atom, &myExpressi onAtom) ;
496
Chapter 16 Wired
WiredUt i I s_AddOperatorAtom (theContai ner, myExpressi onAtom, kOperatorEqual To, &myOperatorAtom) ; myOperandlndex = 1; myConstantValue = 1; WiredUti I s_AddOperandAtom(theContai ner, myOperatorAtom, kOperandConstant, myOperandlndex, NULL, myConstantValue); myOperandlndex = 2; myVariabl eID = kMouseStateVariabl eID; WiredUt i I s AddVariabl eOperandAtom(theContai ner, myOperatorAtom, myOperandlndex, O, NULL, O, myVariableID); WiredUti I s AddActionLi stAtom(theContainer, myConditionaIAtom, &myActionLi stAtom) ; WiredUt i I s AddActi onAtom(theContai ner, myActi onLi stAtom, kActi onSpri teTrans I ate, &myActi onAtom) ; / / f i r s t parameter, get current mouse position x WiredUtil s AddActionParameterAtom(theContainer, myActionAtom, kFirstParam, O, NULL, &myParameterAtom); B
WiredUti I s AddExpressionContainerAtomType(theContainer, myParameterAtom, &myExpressi onAtom) ; WiredUt i I s_AddOperandAtom(theContai ner, myExpressi onAtom, kOperandMouseLocalHLoc, 1, NULL, 0); / / second parameter" get current mouse position y WiredUti I s AddActionParameterAtom(theContainer, myActionAtom, kSecondParam, O, NULL, &myParameterAtom); Wi redUt i I s_AddExpressi onContai nerAtomType(theConta i ner, myParameterAtom, &myExpressi onAtom) ; WiredUt i I s_AddOperandAtom(theContai ner, myExpressi onAtom, kOperandMouseLocalVLoc, 1, NULL, 0); / / third parameter: true mylsAbsol ute = true; WiredUtil s AddActionParameterAtom(theContainer, myActionAtom, kThirdParam, sizeof(mylsAbsolute), &mylsAbsolute, NULL); m
bail: return (myErr) ;
}
Draggable Sprites 497
Before we move on, I should mention that QTWired M a k e S p r i t e D r a g g a b l e isn't completely satisfactory. When you click the cursor on the icon sprite, the sprite is immediately moved so that its registration point lies under the hotspot of the cursor. This is because the kActionSpriteTranslate action moves the registration point of the sprite to the specified location (or offsets it by the specified amount, if the translation is relative). Ideally, we should be able to "grab" the sprite at any point in its image and move it so that the relative positions of the sprite image and cursor remain constant. This is an easy refinement, but one that I'll leave as an exercise for the reader.
Sprite Button Behaviors We've managed to make the icon sprite draggable, but we haven't yet written any code to change the cursor while the icon sprite is being dragged around. A little browsing in Movies.h will reveal the k A c t i o n T r a c k S e t C u r s o r action, which takes one parameter (of type QTAtomID) that specifies the ID of the desired cursor. IDs less than or equal to 1000 are reserved for use by QuickTime. If the ID is 0, then the default system cursor is used. If the ID is greater than 1000, then QuickTime looks in the target track's media property atom for an atom of type 'crsr' with the specified ID; if it finds that atom, it uses the atom's data (which is assumed to be structured just like a Macintosh 'crsr' resource) as the cursor. (On Windows, QuickTime always uses the black-and-white versions of the specified cursors.)Movies.h also defines these constants to allow us to access some built-in cursors {shown in Figure 16.14)" enum {
};
kQTCursorOpenHand
= -19183,
kQTCursorClosedHand
= -19182,
kQTCursorPo i nt i ngHand
= - 19181,
kQTCursorRi ghtArrow
= - 19180,
kQTCursorLe f t A r r o w
= - 19179,
kQTCursorDownArrow
= -19178,
kQTCursorUpArrow
= -19177,
kQTCursorIBeam
= -19176
Figure 16.14 QuickTime's built-in cursors.
498
Chapter 16 Wired
So we do, in fact, k n o w how to add the finishing touches to the icon movie: just add an action atom of type kActionTrackSetCursor to the appropriate event atoms. QuickTime 4 introduced a m u c h simpler and more efficient w a y to attach buttonlike characteristics to a sprite, using sprite button behaviors. We attach these behaviors to a sprite by including an atom of type kSpriteBehaviorsAtomType in the sprite atom. This atom, in turn, contains one to three child atoms that indicate the desired changes that are to be triggered by the state of the mouse button and the location of the cursor: 9 To change the sprite's image on cursor and mouse events, add a child of type kSpriteImageBehaviorAtomType to the sprite behaviors atom. 9 To change the appearance of the cursor on cursor and mouse events, add a child of t y p e kSpriteCursorBehaviorAtomType to the sprite b e h a v i o r s atom. To change the string that's displayed in the status area of a Web browser window, add a child of type kSpriteStatusStringsBehaviorAtomType to the sprite behaviors atom. The atom data in all three of these cases is a structure of type QTSpri teButtonBehaviorStruct, defined like this:
struct QTSpriteButtonBehaviorStruct { QTAtomID notOverNotPressedStateID; QTAtomID overNotPressedStateID; QTAtomID overPressedStateID; QTAtomID notOverPressedStateID;
}~
In a sprite image behavior atom, these fields specify the sprite image indices to use for the specified state. In a sprite cursor behavior atom, these fields specify the cursor IDs to use. In a sprite status strings behavior atom, these fields specify the IDs of sprite string variables. If we don't want the current image or cursor or status string to change w h e n one of these four states is entered, we set the appropriate field to -1. Listing 16.15 shows our definition of the QTWired_AddCursorChangeToSpri te function, which we use to set the custom mouse-over and mouse-down cursors for the icon sprite.
Sprite Button Behaviors 4 9 9
Listing
16.15 Setting a custom cursor for a sprite.
OSErr QTWired AddCursorChangeToSprite (QTAtomContainer theContainer, QTAtomID theID)
{
n
QTAtom QTAtom QTSpri teButtonBehavi orStruct OSErr
mySpriteAtom = O; myBehaviorAtom = O; myBehavi orRec; myErr = paramErr;
/ / find the sprite atom with the specified ID in the specified container mySpriteAtom = QTFindChildByID(theContainer, kParentAtomlsContainer, kSpriteAtomType, theID, NULL); i f (mySpriteAtom == O) goto bai I ; / / insert a new sprite behaviors atom into the sprite atom myErr = QTInsertChild(theContainer, mySpriteAtom, kSpriteBehaviorsAtomType, I, I, O, NULL, &myBehaviorAtom); i f (myErr ! = noErr) goto bai I ; / / set the sprite cursor behavior myBehaviorRec.notOverNotPressedStateID = EndianS32 NtoB(-1); myBehaviorRec.overNotPressedStateID = EndianS32 NtoB(kQTCursorOpenHand) ; myBehaviorRec.overPressedStateID = EndianS32 NtoB(kQTCursorClosedHand) ; myBehaviorRec.notOverPressedStateID = EndianS32 NtoB(-1) ; myErr = QTInsertChild(theContainer, myBehaviorAtom, kSpriteCursorBehaviorAtomType, 1, 1, sizeof(QTSpriteButtonBehaviorStruct), &myBehaviorRec, NULL); bail: return (myErr) ;
}
The actions described in a sprite behaviors atom are inserted at the front of the list of actions associated with a particular event; this allows those behaviors to be overridden by other actions contained in the sprite's event atoms. I'll leave it as an exercise for the reader to r e w o r k the sprite controller track code to use sprite behaviors instead of kActionSpriteSetlmagelndex actions.
500
Chapter 16 Wired
Conclusion In this chapter, we've seen how to use wired actions to construct sprite "buttons" that control a linear QuickTime movie; we've also seen how to make a sprite draggable. Both of these are pretty simple examples, but they do give us a hint of the incredible power waiting to be harnessed. We've used only a handful of sprite actions and only two function operands. So we've got plenty of room to expand our wiring repertoire. In the next two chapters, we'll continue investigating wired actions.
Conclusion 501
This Page Intentionally Left Blank
Moving Target
Introduction In the previous chapter, we got our first taste of working with wired actions. We attached some wired actions to sprites, first to construct a set of "movie controller" sprites and then to make a sprite draggable. In both cases, we accomplished this by inserting one or more event atoms into the sprite atom and then inserting one or more action atoms into those event atoms. In several instances, we also inserted p a r a m e t e r atoms into the action atoms to specify additional information necessary for carrying out the action. In this chapter, w e ' r e going to investigate wired action targets, which are the objects upon which wired actions are performed. Hitherto, we've relied on the fact that any action that has a target also has a default target, upon which the action will be performed if no object is explicitly targeted by that action. Here, we'll see how actions triggered by an event involving one sprite can be made to operate on another sprite, on another track, or indeed on a sprite or track in another movie. Along the way, we'll see how to set targets for operands and add support in our applications for passing actions between movies. Our sample application this time around is called QTActionTargets; its Test m e n u is shown in Figure 17.1. This first m e n u item creates a movie containing two sprites that target each other with actions. The second item adds a sprite track to an existing movie; this sprite track contains a single button that toggles the visibility state of the first text track in that movie. The next four items allow us to get and set movie names and IDs, w h i c h come into play w h e n we want to send actions from one movie to another. The last m e n u item builds a sprite movie that can send actions to two QuickTime VR movies at once.
503
Figure 17.1
The Test menu of QTActionTargets.
Targets Most wired actions perform some operation on an element of a QuickTime movie; this element is called the action's target. For instance, the kActionSpriteTrans] ate action alters the matrix of a sprite so as to translate it horizontally and vertically by specified distances. As we noted in the previous chapter, each wired action has one particular type of target {if it has a target at all, of course). QuickTime currently supports three basic kinds of targets: sprites, tracks, and movies. We specify an action's target by including an atom of type kActionTarget in the action atom. The target atom in turn includes one or more children that specify the kind of target and the method of specifying that target. As we'll see, there are several ways to specify each kind of action target. If an action atom does not contain a target atom, then the action is sent to the default target. The default target for an action that operates on a sprite is the sprite that contains the event atom that triggered the action. (Let's call this the default sprite.) Frame-loaded events have no default sprite, so spriterelated actions contained in a frame-loaded event's action atom must have an explicit target. On the other hand, idle events are sent to each sprite in a track, which is the default sprite for any sprite-related actions it contains. For track actions, the default target is the track that contains the default sprite (or, in the case of frame-loaded events, the track that contains the key frame). For movie actions, the default target is the movie that contains the default track.
Targeting a Sprite We can specify a sprite target in one of three ways: by name, by ID, or by index. The file Movies.h defines these constants for specifying a sprite target type:
504
Chapter 17
Moving Target
enum { kTargetSpri teName kTargetSpriteID kTargetSpri teIndex
};
= FOUR CHAR CODE('spna'), = FOUR CHAR CODE('spid'), = FOUR CHAR CODE('spin') m
m
When we target a sprite by name, QuickTime looks at each sprite atom in the target track for a child atom of type kSpriteNameAtomType. If it finds an atom of this type, it inspects the atom data to see whether it's identical to the name specified in the kTargetSpriteName atom. If it is, that sprite becomes the target sprite. If no sprite in the target track has the correct name, the action is not performed. In general, I prefer to target sprites by ID, largely because we haven't bothered to name our sprites and because the sprite index depends on the order in which we insert sprites into the sprite track. Listing 17.1 shows one of the wired sprite utility functions, WiredUtils_AddSpriteIDActionTargetAtom, which we can use to build a sprite ID target atom and insert it into a given action atom.
Listing 17.1 Targeting a sprite by ID. OSErr Wi redUti I s_AddSpri teIDActionTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, QTAtomID theSpriteID, QTAtom *theNewTargetAtom)
{
QTAtom OSErr
myTargetAtom = O; myErr = noErr;
i f (theNewTargetAtom ! = NULL) *theNewTargetAtom = O; myTargetAtom = QTFindChildBylndex(theContainer, theActionAtom, kActionTarget, 1, NULL); i f (myTargetAtom == O) ( myErr = QTInsertChild(theContainer, theActionAtom, kActionTarget, 1, 1, O, NULL, &myTargetAtom); i f (myErr ! = noErr) goto bai I ;
}
theSpriteID = EndianU32 NtoB(theSpriteID); myErr = QTInsertChild(theContainer, myTargetAtom, kTargetSpriteID, 1, 1, sizeof(theSpriteID), &theSpriteID, NULL); bail: i f (theNewTargetAtom ! = NULL) *theNewTargetAtom = myTargetAtom; return (myErr) ;
}
Targets 505
Figure 17.2 Two sprites targeting each other. WiredUti 1s_AddSpri telDActionTargetAtom starts off by looking for an existing target atom in the specified action atom. If none is found, it adds a target atom. In either case, it then inserts a child of type kTargetSpriteID into the target atom, setting the atom data to the sprite ID we pass in (suitably converted to big-endian form, of course). Let's use WiredUtils AddSpriteIDActionTargetAtom to build a movie in which two sprites target each other. Figure 17.2 shows the movie we want to build; clicking one sprite rotates the other sprite 90 ~ clockwise. (In Figure 17.2, we've clicked the left-hand sprite once.) We construct this movie in the standard way, creating a sprite track and media and then adding sprite images and samples to the media. Listing 17.2 shows the code we use to add actions to the left-hand sprite (which has ID 1). m
Listing 17.2 Wiring the left-hand sprite. WiredUti I s AddQTEventAndActionAtoms(mySpri teData, kParentAtomlsContainer, kQTEventMouseCl i ckEndTriggerButton, kActi onSpri teRotate, &myActionAtom) ; i
myDegrees = EndianS32 NtoB(FixRatio(90, 1)); WiredUti I s AddActionParameterAtom(mySpriteData, myActionAtom, kFi rstParam, sizeof(myDegrees), &myDegrees, NULL); WiredUti I s_AddSpri teIDActionTargetAtom(mySpri teData, myActionAtom, 2, NULL); I
I
We wire the right-hand sprite in exactly the same way, substituting target ID 1 for 2 in the last line of code.
Targeting a Track We can specify a target track in one of four ways: by name, by ID, by index, or by index of a particular type. That is, we can look for the second track in a movie, say, or the second video track in that movie. Movies.h defines these constants for specifying track target types:
506
Chapter 17 Moving Target
enum { kTargetTrackName kTargetTrackID kTargetTrackType kTargetTracklndex
};
= = = =
FOURCHARCODE('trna'), FOURCHARCODE('trid'), FOURCHARCODE('trty'), FOURCHARCODE('trin ~
To specify a target by index of a type, we include both kTargetTrackType and kTargetTrackIndex atoms in the target atom. Once again, we'll want to define some utilities to facilitate creating track targets. Listing 17.3 defines the WiredUtils_AddTrackNameActionTargetAtom function, which sets a track target, given a name. Listing 17.3 Targeting a track by name. OSErr WiredUtils AddTrackNameActionTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, Str255 theTrackName, QTAtom *theNewTargetAtom) D
{
QTAtom OSErr
myTargetAtom = O; myErr = noErr;
i f (theNewTargetAtom [ = NULL) *theNewTargetAtom = O; myTargetAtom = QTFindChildBylndex(theContainer, theActionAtom, kActionTarget, 1, NULL); i f (myTargetAtom == O) { myErr = QTInsertChild(theContainer, theActionAtom, kActionTarget, I, I, O, NULL, &myTargetAtom); i f (myErr l= noErr) goto bai I ;
}
myErr = QTInsertChild(theContainer, myTargetAtom, kTargetTrackName, I, I, theTrackName[O] + I, theTrackName, NULL); bail: i f (theNewTargetAtom l= NULL) *theNewTargetAtom = myTargetAtom; return (myErr) ;
}
Listing 17.4 defines a more general function, WiredUtils_AddTrackTargetAtom, which calls WiredUtils AddTrackNameActionTargetAtom and t~vo other utilities (which we won't show here). Notice that if theTrackTargetType is kTargetTrackType and theTrackTypelndex is nonzero, then WiredUtils AddTrackTargetAtom inserts two target type atoms, as we mentioned previously.
Targets 507
Listing 17.4 Targeting a track. OSErr Wi redUti I s_AddTrackTargetAtom (QTAtomContainer theContai ner, QTAtom theActi onAtom, long theTrackTargetType, void *theTrackTarget, long theTrackTypelndex)
{
OSErr
myErr = noErr;
/ / allow zero for the default target (the sprite track that received the event) i f (theTrackTargetType l= O) { switch (theTrackTargetType) { case kTargetTrackName: { StringPtr myTrackName = (StringPtr)theTrackTarget; myErr = WiredUt i I s AddTrackNameActionTargetAtom(theContai ner, theAct i onAtom, myTrackName, NULL); break; case kTargetTrackID. ( long myTrackID = (long)theTrackTarget; myErr = WiredUtils AddTrackIDActionTargetAtom(theContainer, theActionAtom, myTrackID, NULL); break; case kTargetTrackType. ( OSType myTrackType = (long)theTrackTarget; myErr = WiredUt i I s AddTrackTypeActi onTargetAtom(theContai ner, theAct i onAtom, myTrackType, NULL); i f (myErr l= noErr) goto bai I ; i f (theTrackTypelndex ! = O) myErr = WiredUtil s AddTracklndexActionTargetAtom(theContainer, theActionAtom, theTrackTypelndex, NULL); break; case kTargetTrackIndex: { long myTracklndex = (long)theTrackTarget; myErr = WiredUtils AddTracklndexActionTargetAtom(theContainer, theActionAtom, myTracklndex, NULL); break;
508
Chapter 17 Moving Target
default" myErr = paramErr;
bail: return (myErr) ;
}
To illustrate how to target a track, we'll create a sprite button that enables or disables the first text track in the movie. Figure 17.3 shows this button located in the upper-left corner of the movie; clicking it disables (and thus hides) the first text track in the movie, and clicking it again re-enables (and thus reshows) that track. Let's see how to wire the sprite to achieve this behavior. First, we want to change the sprite image on mouse-down and mouse-up events, like this: Wi redUti I s_AddSpri teSet ImagelndexActi on (myTextButton, kParentAtomlsContainer, kQTEventMouseClick, O, NULL, O, O, NULL, kTextDownlndex, NULL); Wi redUt i I s_AddSpri teSet ImageI ndexAct i on (myTextButton, kParentAtomlsContainer, kQTEventMouseClickEnd, O, NULL, O, O, NULL, kTextUplndex, NULL);
Figure 17.3
A sprite button that targets a text track.
Targets
509
Next, we want to attach some track enabling and disabling logic to the button. Here we have several possibilities. We could read the value of the k0perandTrackEnabled operand to determine w h e t h e r the text track is currently enabled or disabled and then call kActionTrackSetEnabled with parameter false or true, accordingly. Or, m u c h more simply, we can add an action atom of type kActionTrackSetEnabled and then set that atom's action flags to toggle the value of the track's enabled state: WiredUti I s_AddQTEventAndActionAtoms(myTextButton, kParentAtomlsContai ner, kQTEventMouseCl i ckEndTri ggerButton, kActionTrackSetEnabled, &myActionAtom); WiredUti I s AddActionParameterOptions(myTextButton, myActionAtom, 1, kActionFlagActionlsToggle, O, NULL, O, NULL); m
We specify action flags by inserting an atom of type kActionF1 ags into the action atom. The atom ID should be the same as the ID of a parameter atom in that action atom, and the atom data is a big-endian long integer that has one or more of these flags set: enum { kAct i onFl agActi onI sDel ta kActi onFl agParameterWrapsAround kActi onFl agActi onI sToggl e
};
= 1L<<
1,
= 1L < < 2 , = 1L<<3
The flag kActionFlagActionlsDelta indicates that the value of the corresponding parameter atom is a relative value, not an absolute value. The flag kActionF1 agParameterWrapsAround indicates that a value that exceeds the maxi m u m possible value of the parameter is automatically set to the m i n i m u m value (and similarly for values that are less than the m i n i m u m value). We set a parameter's m i n i m u m and m a x i m u m values by inserting atoms of type kActionParameterMinValue and kActionParameterMaxValue into the action atom. The flag kActionFlagActionlsToggle indicates that the parameter value should be toggled between two possible states. In this case, the actual value specified in the parameter atom is ignored. As a result, we haven't bothered to actually add a parameter atom to the kActionTrackSetEnabled action atom. Finally, we call WiredUtils AddTrackTargetAtom to set the sprite action's target to the first text track in the movie, like this: WiredUtils_AddTrackTargetAtom(myTextButton, myActionAtom, kTargetTrackType, (void *)TextMediaType, 1) ;
510
Chapter 17 Moving Target
Movie-to-Movie
Communication
N o w we have a good idea of how to target specific sprites and tracks in a movie. One of the neat additions in QuickTime 4.0 was the ability to target external movies. That is to say, events related to a sprite in one movie are able to trigger actions that operate on a sprite or track or movie p r o p e r t y in another movie. This is called movie-to-movie communication or intermovie communication. In this section, we'll see h o w to target elements of external movies. On one level, this is a very simple thing to do, as it's just a n o t h e r exercise in adding target atoms to action atoms. It turns out, however, that the movie controller needs help resolving target atoms w h e n they pick out external movies. As a result, we'll need to add some code to QTActionTargets to allow it to route actions from one open movie to another. This is a somew h a t more complicated undertaking, so let's proceed carefully.
Adding External Movie Targets First, the easy stuff. We can specify an external movie target in one of two ways: by name or by ID. The file Movies.h defines these two constants for specifying a movie target type" enum { kTargetMovi eName kTargetMovieID
};
= FOURCHAR CODE('mona'), = FOURCHAR CODE('moid') e
To target an action at an object in an external movie specified by name, for instance, we add an atom of type kTargetMovieName to the action atom. The target atom data is the movie's target name, stored as a Pascal string. Listing 17.5 shows the function WiredUtils_AddMovieNameActionTargetAtom, w h i c h we can use to add the appropriate target atom to an action atom.
Listing 17.5 Targeting a movie by name. OSErr WiredUtils AddMovieNameActionTargetAtom (QTAtomContainer theContainer, QTAtom theActionAtom, Str255 theMovieName, QTAtom *theNewTargetAtom)
{
QTAtom OSErr
myTargetAtom= O; myErr = noErr;
i f (theNewTargetAtom ! = NULL) *theNewTargetAtom : O;
Movie-to-Movie Communication
511
myTargetAtom = QTFindChildBylndex(theContainer, theActionAtom, kActionTarget, 1, NULL); i f (myTargetAtom == O) { myErr = QTInsertChild(theContainer, theActionAtom, kActionTarget, 1, 1, O, NULL, &myTargetAtom); i f (myErr ! = noErr) goto bai I ;
}
myErr = QTInsertChild(theContainer, myTargetAtom, kTargetMovieName, 1, 1, theMovieName[O] + i , theMovieName, NULL); bail: i f (theNewTargetAtom ! = NULL) *theNewTargetAtom = myTargetAtom; return (myErr) ;
}
The function WiredUtils AddMovielDActionTargetAtom (not shown here) is entirely analogous, except that the target atom data is a signed long integer specifying the target movie's ID.
Specifying Movie Target Names and lOs So how do we specify a movie's target name or ID? The movie target name is not the name of the movie file that it's contained in (which is good, since some movies exist only in m e m o r y or only as a network stream}. Rather, the movie target name or ID is stored as a piece of movie user data of type 'p] ug'. To specify a movie's target name, we add a piece of movie user data of type 'plug' whose item data is a string of characters of this form: movi ename="name"
And to specify a m o v i e ' s target ID, we add a piece of movie user data of type 'p] ug' whose item data is a string of characters of this form: movi ei d="ID"
User data of type 'plug' was introduced originally for use by the QuickTime Web browser plug-in, for storing parameters for movies e m b e d d e d in Web pages. For instance, to hide the movie controller bar of an e m b e d d e d movie, we can attach a piece of 'plug' user data to the movie whose item data is this string of characters: CONTROLLER="FaI se"
512
Chapter 17 Moving Target
Similarly, we can make a Web-based movie loop during playback by attaching a piece of 'pl ug' user data with this data: LOOP="True"
(QuickTime Player ignores all user data items of type 'plug' except for moviename and movieid.) The data in a user data item of type 'pl ug' consists of a tag, the equality sign (=), and a value that is enclosed in quotation marks ("). (In fact, the quotation marks are optional, but they're highly recommended.) The tag and value are strings of characters; case does not matter in these strings, so "moviename" is treated the same as "MOVIENAME" (or as any of the other 510 possibilities). In our code, we'll always create all-lowercase tags, using these constants: #define kMovieNamePrefi x #define kMovieIDPrefix
"movi ename=" "mov i ei d="
Listing 17.6 defines the function QTUtils_SetMovieTargetName, w h i c h we
can use to attach a specific target name to a movie. The meat of this function consists of concatenating the prefix kMovieNamePrefix, a quotation mark, the specified target name, and a terminating quotation mark into a single string, which we set as the user item data. Listin 9 17.6 Setting a movie's target name. OSErr QTUtils SetMovieTargetName (Movie theMovie, char *theTargetName)
(
UserData char Handle OSErr
myUserData= NULL; *myString = NULL; myHandle = NULL; myErr = noErr;
/ / make sure we've got a movie and a name i f ((theMovie == NULL) II (theTargetName == NULL)) return (paramErr) ; / / get the movie's user data l i s t myUserData = GetMovieUserData(theMovie) ; i f (myUserData == NULL) return (paramErr) ;
Movie-to-Movie Communication
513
/ / remove any existing movie target name while (QTUtils FindUserDataltemWithPrefix(myUserData, FOURCHAR CODE('plug'), kMovieNamePrefix) ! = O) RemoveUserData(myUserData, FOURCHAR CODE('plug'), QTUtils FindUserDataltemWithPrefix(myUserData, FOURCHAR CODE('plug'), kMovi eNamePrefi x) ) ; / / create the user data item data myString = malloc(strlen(kMovieNamePrefix) + strlen(theTargetName) + 2 + 1); i f (myString l= NULL) { myStri ng [0] = ' \ 0 ' ; strcat(myString, kMovieNamePrefix) ; strcat(myString, "\"") ; strcat(myString, theTargetName) ; strcat(myString, "\ ....); / / add in a new user data item PtrToHand(myString, &myHandle, strlen(myString)) ; i f (myHandle l= NULL) myErr = AddUserData(myUserData, myHandle, FOUR_CHAR_CODE('plug')); } else { myErr = memFull Err;
}
free(myStri ng) ; i f (myHandle l= NULL) Di sposeHandl e (myHandle) ; return (myErr) ;
Notice that QTUtils_SetMovieTargetName first removes any existing movie target name user items by repeatedly calling QTUtils_FindUserDataltemWithPrefix and RemoveUserData. QTUtils FindUserDataltemWithPrefix is defined in Listing 17.7. It simply inspects all user data items of a specified type to see whether any of them begins with the specified string. When the first matching item is found, the index of that item is returned to the caller.
514
Chapter 17 Moving Target
Listing
17.7
Finding a user data item.
static long QTUtils FindUserDataltemWithPrefix (UserData theUserData, OSType theType, char *thePrefix) Handle long 1ong long OSErr
myData = NULL; myCount = O; mylndex = O; myltemlndex = O; myErr = noErr;
/ / make sure we've got some valid user data i f (theUserData =- NULL) goto bai I ; / / allocate a handle for GetUserData myData = NewHandle(O); i f (myData == NULL) goto bai I ; myCount = CountUserDataType(theUserData, theType) ; for (mylndex = I; mylndex <= myCount; mylndex++) { myErr = GetUserData(theUserData, myData, theType, mylndex); i f (myErr == noErr) { i f (GetHandleSize(myData) < strlen(thePrefix)) continue; / / see i f the user data begins with the specified prefix i f (IdenticaIText(*myData, thePrefix, strlen(thePrefix), strlen(thePrefix), NULL) == O) { myltemlndex = mylndex; goto bai I ;
}
bail: i f (myData i= NULL) Di sposeHandl e (myData) ; return (myltemlndex) ;
Movie-to-Movie Communication
515
IdenticaIText (n6 IUMagIDPString) is a Text Utilities function that compares two strings to see whether they are identical; it is case-insensitive and
ignores any diacritical marks (such as the accent in "n6").
Retrieving Movie Target Names and IDs We know how to set a movie's target name or ID so that actions in other movies can be targeted at it. Let's now see how to get a movie's target name or ID. The basic idea is simple enough: just loop through all user data items of type 'plug' until we find one that begins with either kMovieNamePrefix or kMovieIDPrefix; then extract the value of that user data item. Listing 17.8 shows the QTUti I s_GetMovi eTargetName function. (OTUti 1s_GetMovi eTargetID is entirely analogous, except that it also returns a Boolean value that indicates whether the specified movie actually has a movie target ID.) Listing 17.8 Getting a movie's target name. char *QTUtils GetMovieTargetName (Movie theMovie)
(
UserData char
myUserData = NULL; *myString = NULL;
/ / make sure we've got a movie i f (theMovie == NULL) goto bai I ; / / get the movie's user data l i s t myUserData = GetMovieUserData(theMovie) ; i f (myUserData == NULL) goto bai I ; / / find the value of the user data item of type 'plug' that begins with / / the string "moviename='' myString = QTUtils GetUserDataPrefixedValue(myUserData, FOURCHARCODE('plug'), kMovieNamePrefix) ; bail: return (myStri ng) ;
}
As you can see, QTUtils_GetMovieTargetName calls QTUtils_GetUserDataPrefi xedVal ue, defined in Listing 17.9. QTUti I s_GetUserDataPrefi xedVal ue calls QTUtils FindUserDataltemWithPrefix to And the index of the 'plug' user
data item that begins with the specified prefix. If it finds one, then it returns the value in the item's data, making sure to strip off any quotation marks surrounding the value. 516
Chapter 17 Moving Target
Listing
17.9
Getting a user data item value.
s t a t i c char *QTUtils GetUserDataPrefixedValue (UserData theUserData, OSType theType, char *thePrefi x) long Handle long long char OSErr
myl ndex = 0; myData = NULL; myLength = O; myOffset = O; *myStri ng = NULL; myErr = noErr;
i f (theUserData == NULL) goto bai I ; / / allocate a handle for GetUserData myData = NewHandle(O) ; i f (myData -= NULL) goto bai I ; mylndex = QTUtils FindUserDataltemWithPrefix(theUserData, theType, thePrefix) ; i f (mylndex > O) { myErr = GetUserData(theUserData, myData, theType, mylndex); i f (myErr == noErr) { i f ((*myData)[strlen(thePrefix)] == ' " ' ) { myLength = GetHandleSize(myData) - s t r l e n ( t h e P r e f i x ) - 2; myOffset = 1; } else { myLength = GetHandleSize(myData) - s t r l e n ( t h e P r e f i x ) ; myOffset = O;
}
myString = malloc(myLength + 1); i f (myString != NULL) { memcpy(myString, *myData + s t r l e n ( t h e P r e f i x ) + myOffset, myLength); myStri ng [myLength] = ' \ 0 ' ;
}
bail: i f (myData != NULL) Di sposeHandl e (myData) ; return (myStri ng) ;
Movie-to-Movie Communication
517
Finding Movie l"argds R e m e m b e r that we don't just want our applications to be able to create wired actions with external movie targets, but we also want t h e m to be able to play those movies back correctly, routing actions to the appropriate external targets. This raises a complication: although a movie controller is able to find target sprites and tracks inside of the movie it's associated with, it isn't able to find targets in external movies. For that, it needs help from the application. When a movie controller determines that it needs to find an external movie target for some wired action, it sends itself the mcActionGetExternalMovie movie controller action. The idea is that the application will intercept that action in its movie controller action filter function, find the specified target movie, and return information about that movie to the movie controller. Let's see how QTActionTargets handles all this. W h e n our movie controller action filter function receives the mcActi onGetExternalMovie action, the first parameter is the movie controller issuing the action and the third parameter is a pointer to an external movie record of type QTGetExterna I Movi eRecord: struct QTGetExternalMovieRecord { 1ong target Type; Stri ngPtr movi eName; long movieID; Movi ePtr theMovi e; Movi eControl I erPtr theControl I er;
The targetType field specifies the kind of target about which the movie controller wants information; on entry to our movie controller action filter function, it's set to either kTargetMovieName or kTargetMovieID. If targetType is kTargetMovieName, then the movieName field specifies the name of the movie to look for. If targetType is kTargetMovieID, then the movieID field specifies the ID of the movie to look for. Our job is to find the target movie and return both it and its associated movie controller in the theMovie and theController fields of the external movie record. If we cannot find the target movie, then we should set both theMovie and theController to NULL. Listing 17.10 shows the movie controller action filter function in QTActionTargets.
518
Chapter 17 Moving Target
Listing 17.10 Handling movie controller actions. PASCAL_RTN Boolean QTApp_MCActionFilterProc (MovieController theMC, short theAction, void *theParams, long theRefCon)
{
Boolean WindowObject
i sHandl ed = fal se; myWindowObject = NULL;
myWindowObject = (Wi ndowObject) theRefCon; i f (myWindowObject == NULL) return (i sHandl ed) ; switch (theActi on) { / / handle window resi zing case mcActionControl lerSizeChanged" QTFrame Si zeWindowToMovie (myWindowObject) ; break; / / handle idle events case mcActionIdle" QTApp_Idl e ( (**myWi ndowObject). fWi ndow) ; break; / / handle get-external-movie requests case mcActionGetExternal Movie" QTFrame_FindExternaIMovieTarget(theMC, (QTGetExternaIMoviePtr)theParams) ; break; / / some lines missing here; see Listings 17.13 and 17.14
}
default. break;
return (i sHandl ed) ;
As you can see, when we receive an mcActionGetExternalMovie action, we call the application-defined function OTFrame_FindExternalMovi eTarget. It loops through all open movie windows belonging to our application and (depending on the value of the targetType field) looks for a movie having the correct name or ID. QTFrame_FindExternalMovieTarget is defined in Listing 17.11.
Movie-to-Movie Communication
519
Listing 17.11 Finding a target movie. void QTFrame FindExternaIMovieTarget (MovieController theMC, QTGetExternaIMovi ePtr theEMRecPtr) u
WindowReference Movie Movi eControl I er Bool ean
myWindow = NULL; myTargetMovi e = NULL; myTargetMC = NULL; myFoundlt = false;
i f (theEMRecPtr == NULL) return; / / loop through all open movies until we find the one requested myWindow = QTFrame GetFrontMovieWindow(); while (myWindow I= NULL) { Movie myMovie = NULL; MovieController myMC = NULL; myMC = QTFrame_GetMCFromWindow(myWindow) ; # i f ALLOW SELF TARGETING i f (myMC != NULL) { #else i f ((myMC ! = NULL) && (myMC I= theMC)) { #endi f myMovie = MCGetMovie(myMC); D
if
(theEMRecPtr->targetType == kTargetMovieName) { char *myStr = NULL; myStr = QTUti I s GetMovieTargetName(myMovie) ; i f (myStr l= NULL) { i f ( I dent i ca I Text (&theEMRecPtr->movi eName[ 1], myStr, theEMRecPtr->movieName[O], strlen(myStr), NULL) == O) myFoundlt = true; free (myStr) ;
}
if
520
(theEMRecPtr->targetType == kTargetMovieID) { long myID = O; Boolean myMovieHasID= false;
Chapter 17 Moving Target
mylD = QTUtils GetMovieTargetlD(myMovie, &myMovieHaslD) ; i f ((theEMRecPtr->movielD == mylD) && myMovieHaslD) myFoundlt : true; if
}
(myFoundlt) { myTargetMovi e = myMovie; myTargetMC = myMC; break; / / break out of while loop
myWindow = QTFrame_GetNextMovieWi ndow(myWindow) ;
*theEMRecPtr->theMovi e = myTargetMovi e; *theEMRecPtr->theControl I er = myTargetMC;
QTFrame_FindExternalMovieTarget uses our framework functions QTFrame_ GetFrontMovieWindow and QTFrame GetNextMovieWindow to loop t h r o u g h the open movie windows; it also uses the QTActionTargets functions QTUti l s_
GetMovieTargetName and QTUtils GetMovieTargetI9 to find the target name or ID of the movie in each of those windows. {These latter two functions are defined in the file QTActionTargets.c, but they are generally useful and should probably migrate to QTUti 1i t i es. c.) There is one interesting question we need to consider: should a movie be allowed to target itself using external movie targets? That is to say, w h e n we attach a target atom of type kTargetMovieName or kTargetMovieID to some wired action, should we allow the targeted movie to be the same movie that contains that wired action? On one hand, targeting oneself using movie targets is outrageously inefficient; the movie controller needs to call the application's movie controller action filter function, and the application needs to go looking at the user data of all open movie windows {at least until it finds the targeted movie). If our goal is to target the movie that contains the action, we're better off just omitting any movie target atom from that action and thus using the default movie target. On the other hand, we might think: why not allow it? After all, a target ID might be obtained dynamically by evaluating some expression, which [as it happens at runtime) picks out the very movie that contains the wired action container. As long as we're clever about it, efficiency should not be a real problem. And no doubt some interesting effects can be achieved if we allow self-targeting.
Movie-to-Movie Communication
521
Personally, I'm inclined to think that a movie should be able to target itself. Unfortunately, QuickTime Player seems to think differently; it does not currently allow wired actions to target objects in the same movie using an explicit movie target. So I've written QTFrame_FindExternalMovieTarget to provide either capability, depending on the setting of the compiler flag ALLOW_SELF_TARGETING.You decide how you want your application to behave.
Controlling External Movies Let's build a movie with some sprites that target an external movie. In fact, let's build a movie with some sprites that target two external movies at the same time. Figure 17.4 illustrates what I want to achieve. This is a Web browser window that contains two QuickTime VR panorama movies and a sprite movie. The top movie is a panorama of the Donner Lake area (in California) shot during the summer. The bottom movie is a panorama of the same area shot during the winter. Each of these panoramas can be individually controlled in the standard ways (for example, panned left or right by dragging the cursor horizontally). But these panoramas can also be controlled in tandem using the middle movie, which is a sprite movie containing six sprites. The sprite movie shown in Figure 17.4 consists of a single sprite track with buttons that perform these actions (from left to right): pan left, tilt down, zoom out, zoom in, tilt up, and pan right. The VR controller sprite track is constructed in exactly the same way as the linear controller sprite track we built in the previous chapter, except that the sprite images are different and the actions issued by each button are VR actions [for instance, kActionOTVRSetTiltAngle). Also, I've wired the buttons to respond to mouse-
over events, not mouse-click events. Targeting two external movies with the same sprite action is extremely simple. Let's suppose that the summer and winter panoramas have the target names "Summer" and "Winter", respectively. Then we can wire the "Pan Left" sprite using the code shown in Listing 17.12. Listing 17,12. Wiring a sprite to control two external movies. #defi ne kTargetl #defi ne kTarget2
"Summer" "Winter"
Stri ngPtr
myMovieNames[2] ;
myMovieNames[O] = QTUtils ConvertCToPascaIString(kTargetl) ; myMovieNames[ 1] -- QTUti I s ConvertCToPascalString (kTarget2) ; m
for (myCount = O; myCount <= 1; myCount++) { SpriteUtils_SetSpriteData(myPanLeftButton, &myLocation, &isVisible, &myLayer, &mylndex, NULL, NULL, NULL);
52,2
Chapter 17 Moving Target
WiredUtils AddSpriteSetlmagelndexAction(myPanLeftButton, kParentAtomIsContainer, kQTEventMouseEnter, O, NULL, O, O, NULL, kPanLeftDownlndex, NULL); WiredUti I s AddSpriteSetlmagelndexAction(myPanLeftButton, kParentAtomlsContainer, kQTEventMouseExit, O, NULL, O, O, NULL, kPanLeftUplndex, NULL); WiredUti I s AddSpriteTrackSetVari abl eActi on (myPanLeftButton, kParentAtomlsContai ner, kQTEventMouseEnter, kMouseOverPanLeftVariableID, 1, O, NULL, 0); WiredUti I s AddSpriteTrackSetVari abl eActi on (myPanLeftButton, kParentAtomlsContai ner, kQTEventMouseExit, kMouseOverPanLeftVariableID, O, O, NULL, 0); QTTarg_AddIdl eEventVarTestActi on (myPanLeftButton, kParentAtomlsContai ner, kMouseOverPanLeftVariableID, 1, kActionQTVRSetTiltAngle, &myActionAtom); WiredUti I s AddActionParameterAtom(myPanLeftButton, myActionAtom, kFi rstParam, sizeof(myDeltaAngle), &myDeltaAngle, NULL); WiredUtils AddActionParameterOptions(myPanLeftButton, myActionAtom, 1, kActionFlagActionIsDelta, O, NULL, O, NULL); WiredUtils AddMovieNameActionTargetAtom(myPanLeftButton, myActionAtom, myMovieNames[myCount], NULL); m
}
The key element here is the call to Wi redUtil s_AddMovieNameActionTargetAtom, which adds the appropriate movie name target atom to the action atom. You'll also notice that we call the application function OTTarg_AddIdleEventVarTestAction, which installs an idle event action that checks to see w h e t h e r the specified variable (in this case, the one with ID kMouse0verPanLeftVariableIO) has the value specified in the fourth parameter; w h e n e v e r it does, the specified action is executed. (See QTActionYargets.c for the complete listing of OTTarg_AddldleEventVarTestAction.)
R e t r i e v i n g a M o v i e ' s O w n T a r g e t N a m e and ID QuickTime 5 introduced two new operands related to movie target n a m e s and IDs, k0perandMovieName and k0perandMovieIO. Wired actions can use these operands to determine the n a m e and ID of a target movie. Once again, the movie controller handling these operands needs some help from our application to get the desired information. In a sense, this is the flip side of the task we considered earlier. In that case, a movie controller had a movie's target name or ID, and we needed to find the movie and movie controller associated with that name or ID. In the present case, the movie controller is known, and we need to find the movie target name or ID associated with that movie controller.
Movie-to-Movie Communication 523
Figure 11.4 A sprite movie controlling two QuickTime VR movies. W h e n a movie controller encounters the kOperandMovieNameor kOperandMovielD operand and hence needs to get a movie's n a m e or ID, it sends the mcActionGetMovieName or mcActionGetMovieID movie controller action to the movie controller associated with that movie. Our application should respond to these actions by returning the movie target n a m e or ID. Let's consider
524
Chapter 17 Moving Target
first the mcActionGetMovielD action. W h e n our movie controller action filter function receives this action, the theParams p a r a m e t e r is a pointer to a long integer. If the movie indeed has a movie target ID, we should r e t u r n that ID in that long integer and also return true as the r e t u r n value of our filter function. Otherwise, we should return f a l s e as the r e t u r n value of our filter function. Listing 17.13 shows the lines of code in our movie controller action filter function that handle the mcActionGetMovieID action.
Listing
17.13 Getting a movie's target ID.
case mcActionGetMovi eID: myMovie = (**myWindowObject). fMovie; i f (myMovie ! = NULL) { long myID; Boolean myMovieHasID= false; / / get the target name of the current movie myID = QTUtils GetMovieTargetID(myMovie, &myMovieHasID) ; i f (myMovieHasID) { *(long *)theParams = myID; isHandled = true; m
}
break; As you can see, this code uses the QTUtils_GetMovieTargetID function to
get the target ID of the current movie. If the movie has no target ID, we don't put any value into theParams and we allow the filter function to return the default value of fa] se. W h e n our movie controller action filter function receives the mcAct i onfietMovieName action, the theVarams p a r a m e t e r is a handle to a Pascal string. In this case, we need to determine the target n a m e of the movie and copy it into that Pascal string. Listing 17.14 shows the code that handles this.
Listing
17.14 Getting a movie's target name.
case mcActi onGetMovi eName: myMovie = (**myWindowObject). fMovie; i f (myMovie ! = NULL) { char *myName = NULL; Handle myNameHandl e = (Handle) theParams;
Movie-to-Movie Communication
525
/ / get the target name of the current movie myName = QTUtil s GetMovieTargetName(myMovie) ; I
i f ((myName I= NULL) && (myNameHandle I= NULL)) { / / reset the size of the handle passed to us to hold the movie target name SetHandleSize(myNameHandle, strlen(myName) + 1); i f (MemError() == noErr) { / / copy the movie target name into the resized handle BlockMove(myName, *myNameHandle + 1, strlen(myName)) ; *myNameHandle[O] - strlen(myName) ; isHandled = true;
}
free (myName) ; break;
Here, we use our function QTUtils_GetMovieTargetName (defined in Listing 17.8) to get the movie target name. Notice that we need to resize the handle passed to us in order to ensure that it's large enough to hold the name we pass back.
Operand Targets Recall that wired action operands typically retrieve properties of some element of a movie, such as a track's height, a movie's duration, a sprite's image index, a sprite track variable's value, and so forth. The element of the movie whose property is to be retrieved is the operand target. As with actions, each operand that has a target also has a default target. But also as with actions, it's possible to specify some other target for an operand by inserting a target atom into the operand atom. So, we can say things like: get me the image index of the sprite named "Old QuickTime icon" in the second sprite track of the movie whose target ID is 11. In this section, we're going to play a little with operand targets; along the way, we'll discover that they can sometimes be a bit trickier than they seem. Let's begin by targeting a wired action (and not an operand) at an object in an external movie. Consider once again the moving icon movie that we constructed in Chapter 14, "A Goofy Movie," shown in Figure 17.5. Let's assume that this movie contains the 'plug' user data necessary to set its target name to "Icon Movie". Now suppose that we've created some other movie with a sprite track, maybe the one shown earlier in Figure 17.3. (Let's also assume that the tar-
5216 Chapter 17 Moving Target
Figure 17.5 The moving icon movie. get name of this movie is "Button Movie".) We want to rewire the sprite button so that clicking on the button changes the image index of the sprite in the moving icon movie. W h e n we first open the moving icon movie, the image index of the icon sprite is 1. So let's reset that index to 2. With the machinery we have at hand, this is pretty straightforward: we just insert a kActionSpriteSetImageIndex action atom into the button sprite atom, together with a parameter atom whose atom data is the constant 2. We also add to the action atom a target atom that picks out the sprite with ID 1 in the first sprite track in the movie whose target name is "Icon Movie". Listing 17.15 shows our revised wiring. Listing 17.15 Setting the image index of an external sprite. #define kTargetName
"Icon Movie"
WiredUti I s AddSpriteSetlmagelndexAction(myTextButton, kParentAtomlsContainer, kQTEventMouseClick, O, NULL, O, O, NULL, kTextDownlndex, NULL); WiredUti I s_AddSpri teSet ImagelndexActi on (myTextButton, kParentAtoml sContai ner, kQTEventMouseClickEnd, O, NULL, O, O, NULL, kTextUplndex, NULL); WiredUt i I s_AddQTEventAndActi onAtoms(myTextButton, kParentAtoml sContai ner, kQTEventMouseClickEndTriggerButton, kActionSpriteSetImagelndex, &myActionAtom) ; mylndex = EndianS16 NtoB(2) ; WiredUtil s AddActionParameterAtom(myTextButton, myActionAtom, kFirstParam, sizeof(mylndex), &mylndex, NULL) WiredUti I s AddTrackAndSpriteTargetAtoms (myTextButton, myActionAtom, kTargetTrackType, (void *)SpriteMediaType, 1, kTargetSpriteID, (void *)1) ; D
myString = QTUti I s ConvertCToPascalString(kTargetName) ; WiredUtils AddMovieTargetAtom(myTextButton, myActionAtom, kTargetMovieName, (void *)myString) ; free(myStri ng) ; m
Operand Targets 527
So far, so good. Now let's see w h a t happens if we try to specify the n e w image index using an expression that contains an operand. For instance, let's try to read the image index from a sprite track variable. First, of course, we need to initialize the variable; we can do this with a frame-loaded event action, like so: float QTAtomID
myVariableValue = 2; myVari abl eID = 1234;
WiredUt i I s_AddSpri teTrackSetVari abl eAct i on (mySample, kParentAtomIsContai ner, kQTEventFrameLoaded, myVariableID, myVariableValue, O, NULL, 0);
Now we need to revise the sprite button wiring to use that variable instead of the hard-coded constant 2. Listing 17.16 shows our first try. Listing 17.16 Setting the image index of an external sprite using a variable. WiredUti I s AddQTEventAndActionAtoms(myTextButton, kParentAtomIsContainer, kQTEventMouseCl ickEndTriggerButton, kActionSpriteSetlmagelndex, &myActionAtom) ; QTInsertChild(myTextButton, myActionAtom, kActionParameter, O, kFirstParam, O, NULL, &myParamAtom); QTInsertChild(myTextButton, myParamAtom, kExpressionContainerAtomType, 1, 1, O, NULL, &myExpressionAtom); QTInsertChild(myTextButton, myExpressionAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); QTInsertChi I d(myTextButton, myOperandAtom, kOperandSpriteTrackVari abl e, 1, 1, O, NULL, &myOperandTypeAtom); myVariableID = EndianU32 NtoB(myVariableID); QTInsertChild(myTextButton, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); WiredUti I s AddTrackAndSpriteTargetAtoms (myTextButton, myActionAtom, kTargetTrackType, (void *)SpriteMediaType, 1, kTargetSpriteID, (void *)1) ; myString = QTUti I s ConvertCToPascalString(kTargetName) ; WiredUti I s AddMovieTargetAtom(myTextButton, myActionAtom, kTargetMovieName, (void *)myString) ; free(myStri ng) ; m
528
Chapter 17 Moving Target
There's only one problem: this doesn't work. The reason is that the sprite button movie's movie controller sees that the kActionSpriteSetImageIndex action is targeted at the icon movie, so it sends the entire action atom over to the movie controller of the icon movie for handling. When the movie controller of the icon movie tries to get the value of the sprite track variable with ID 1234, it discovers that there is no such variable defined yet. In that case, it sets the icon's image to 0 [which is the default value of all uninitialized sprite track variables). Instead of changing the image index to 2, the movie controller changes it to 0 and the icon sprite promptly disappears. Oops. This is where operand targets come into play. What we need to do is attach a target atom to the k0perandSpriteTrackVariable operand that picks out the sprite track that contains the sprite button (that is, the track in which we set the value of the variable with ID 1234). Listing 17.17 shows the essential part of our re-revised wiring. Listing 17.17 Adding a target to an operand atom. #define kButtonName
"Button Movie"
/ / e x p l i c i t l y target the sprite track and movie for the operand myString = QTUtiI s ConvertCToPascalString(kButtonName) ; WiredUti I s AddTrackAndSpriteTargetAtoms (myTextButton, myOperandTypeAtom, kTargetTrackType, (void *)SpriteMediaType, i, kTargetSpriteID, (void *)1); WiredUti I s AddMovieTargetAtom(myTextButton, myOperandTypeAtom, kTargetMovieName, (void *)myString) ; free(myStri ng) ; m
m
This works fine, but it seems to violate the "no self-targeting" rule adopted by QuickTime Player. After all, the sprite track in the movie "Button Movie" explicitly targets the movie "Button Movie". So we might expect this wiring to work in QTActionTargets but not in QuickTime Player, right? Nope, it works fine in both applications. Remember that when the movie controller of the button movie detects an action targeted at an external movie, it sends the action atom off to the movie controller of that movie. The target atom in the operand atom is evaluated and resolved only by the movie controller of the icon movie, in which case it's not a self-reference at all; rather, it's now targeting the sprite button movie. So both applications, QTActionTargets and QuickTime Player, are able to find the track containing the relevant variable and retrieve its value. There are a couple of important lessons to take away from all this. First and foremost, action targets and operand targets are resolved only at runtime. As a result, the target of an action or operand can change if some of
Operand Targets 529
the properties of the movie change. (For instance, the sprite with a certain ID might differ from the sprite with that same ID, if the movie time is changed so that a new key frame of the sprite track is loaded.I Also, a target atom contained in an action or operand atom is always interpreted relative to any target atoms contained in the parents of that action or operand atom. If a parent action or operand atom changes its target to some other sprite, track, or movie, then all its children will inherit that change. The only w a y to be absolutely sure that we know what an action or operand is targeting is to build the atom hierarchy very carefully or to give a full, explicit target.
Movie-in-Movie Communication Let's finish up with something perhaps a little less dizzying. In Chapter 8, "The Atomic Caf6," we learned how to use the movie media handler to embed one movie (called the child moviel inside of another movie (the parent movie). The main advantage of using this movie-in-movie capability is that the parent and child movies can have independent playback characteristics. This means that the looping state or the playback rate of the parent movie can differ from the looping state or the playback rate of the child movie. We embed one movie inside of another movie by creating a movie track, whose samples are atom containers that contain atoms that pick out the child movies and specify their playback characteristics. (Again, see Chapter 8 for more complete details on creating movie tracks.I In this section, we'll see how to work with embedded movies and wired actions. Things w o r k pretty much as you'd expect, but there are a couple of new capabilities that deserve attention.
Specifying Movie-in-Movie Targets We can send wired actions from a parent movie to a child movie and from a child movie to a parent movie. If we want to send a wired action from a child to a parent, we can target the parent using one of these two target types: enum {
};
kTargetRootMovi e kTargetParentMovi e
= FOUR CHAR CODE('moro'), = FOUR CHAR CODE('mopa') m
A child's parent movie, of course, is the movie that contains the movie track that references the child movie. The root movie is the movie at the top of the parent/child hierarchy. When a parent movie contains a child movie that does not itself contain any movie tracks, then the parent and the root are
53Q Chapter 17 Moving Target
the same. But a child movie can easily contain its own children (it is after all a movie, so it can contain movie tracks), and this containment hierarchy can go indefinitely deep. So it's useful to have a w a y to target the movie at the root of the hierarchy, and that's w h a t kTargetRootMovi e gives us. A child movie is a movie that's picked out by a data reference c o n t a i n e d in a sample in a movie track. So it has a sort of "dual nationality": we can target it as if it were a movie or as if it were a track. To target it as a movie, we can target its movie ID or its movie name. And to target it as a track, we can target its track ID, its track name, or its track index. There are therefore five ways for a parent movie (or some element of a parent movie) to target a child movie. The file Movies.h defines these five constants for specifying a child movie target type: enum { kTargetChi I dMovieTrackName kTargetChi I dMovieTrackID kTargetChi I dMovieTrackl ndex kTargetChi I dMovieMovieName kTargetChi I dMovieMovieID
};
= = = = =
FOUR CHAR CODE('motn'), FOUR CHAR CODE('moti ' ) , FOUR CHAR CODE('motx'), FOUR CHAR CODE('momn'), FOUR CHAR CODE('momi') m
B
These are all easy enough to figure out, except perhaps for kTargetChi I dMovieTrackIndex. An atom of this type targets a track with a specific index in a movie, w h i c h had better be a movie track (that is, a track of type Movi eMediaType). As far as I know, we cannot directly target a child movie by its index a m o n g just the movie tracks.
Using Movie-Loaded Events QuickTime 4.1, w h i c h introduced the movie-in-movie capability and the five new target types, also introduced a new kind of event for triggering w i r e d actions, the movie-loaded event. A movie-loaded event [of type kQTEventMovi eLoaded) is issued w h e n e v e r a child movie is loaded into a parent movie. We can construct wired event handlers to look for this event and execute wired actions w h e n it is received. The movie-loaded event is particularly useful for configuring the child movie based on some of the current properties of its parent, or conversely for configuring the parent based on some of the properties of the child. Event atoms of type kQTEventMovieLoaded can be put into the parent movie or into a child movie. In a parent movie, a movie-loaded event a t o m is placed into the atom container that comprises a sample in a movie track, as a sibling of the kMovieMediaOataReference atom [which picks out the child movie). In a child movie, a movie-loaded event atom is inserted into the
Movie-in-Movie Communication 531
movie property atom. A movie property atom is an atom container that contains information about the movie's properties, in just the same w a y that a track's media property atom (which we encountered in Chapter 14, "A Goofy Movie") contains information about the properties of the track and its media. We can insert a movie-loaded event and its accompanying actions into a movie property atom by calling the GetMoviePropertyAtom function, inserting the atom, and then calling SetMovi ePropertyAtom. If a parent movie and a child movie both contain movie-loaded event atoms, then the actions in the child movie's event atom are executed first, followed by those in the parent movie's event atom. That is to say, the parent gets the final word here. (Who says movies don't accurately portray reality?) For instance, if both the child movie and the parent movie set the value of some sprite track variable, then the parent's assignment overrules the child's.
Conclusion In this chapter, we've touched on just about every possible way of targeting a wired action or operand at some element of a movie, no matter w h e r e that element resides. The target could be in the same track as the event atom that triggers the action or in another track; it could also be in an external movie or in a child or parent movie. In all these cases, we link the action or operand to the target by inserting an atom of type kActionTarget into the action or operand atom. The movie controller is responsible for resolving these target atoms at runtime and dispatching the actions or operands to the appropriate target. We've also seen that the movie controller sometimes needs our help in finding targets; if an action or operand targets an external movie, our application needs to step in and find the specified target movie. In the next chapter, we'll take one final look at wired actions (a topic we'll return to in QuickTime Toolkit, Volume Two: Advanced Movie Playback and Media Types). We'll see that wired action atoms can be associated with objects other than sprites, and we'll investigate some of the newer event types and actions.
532
Chapter 17 Moving Target
Back In Action
Introduction The previous four chapters have focussed largely on two topics: how to create and manipulate sprites and how to attach dynamic, interactive behaviors to sprites. Interactivity is one of QuickTime's greatest assets and perhaps its most significant advantage over competing media architectures. And wired sprites are certainly the cornerstone of QuickTime interactivity. As w e ' v e seen, we can do some pretty nifty things with just a few event and action atoms placed in the right locations in a sprite track. In this chapter, w e ' r e going to take one more look at wired actions. I want to consider a few of the event types and actions introduced in QuickTime 5, and I especially want to consider how to use wired actions with other kinds of tracks. So far, all the actions w e ' v e encountered have been triggered by some event involving a sprite or a sprite track. Beginning in QuickTime 4, however, it's also possible to attach wired actions to QuickTime VR nodes and hotspots, to text in text tracks, and to Flash content. In a QuickTime VR movie, we can attach wired actions to a node and to hotspots in a node. The node actions can be attached to idle events {so that an action can be triggered periodically) or to frame-loaded events {so that an action can be triggered w h e n the node is first entered). We could, for instance, attach an action to a frame-loaded event to set the initial pan and tilt angles that are displayed w h e n the user enters the node. Similarly, we could attach an action to a frame-loaded event to start a sound track playing w h e n the user enters the node. In addition, we could use idle event actions to dynamically adjust the balance and volume of that sound as the user changes the pan angle of the movie; this provides an easy way to make a sound appear to emanate from a specific location in a node {which is sometimes called directional sound). Or, we could adjust the pan and tilt angles of some other QuickTime VR movie, so that panning and tilting one p a n o r a m a caused similar panning and tilting in a second.
533
We can also attach wired actions to specific hotspots inside of QuickTime VR nodes. We can configure these actions to be triggered by a variety of mouse events, including moving the mouse over a hotspot, moving the mouse out of a hotspot, and clicking the mouse within a hotspot. Once again, the actions that are triggered by these events can be any actions supported by the QuickTime wired actions architecture, such as starting and stopping sounds, enabling and disabling tracks, changing pan and tilt angles, and so forth. We can attach wired actions to a text track, giving us wired text. Consider, for instance, the movie shown in Figure 18.1. This movie contains a text track that's wired to display the amount of memory currently available in the application's heap. (The text track is updated every two seconds by an idle event action.) We can use this movie to track the memory usage of any QuickTime-savvy application. Or, consider the movie shown in Figure 18.2, which also contains a single text track. This time the text track is wired so that clicking the word "Apple" launches the user's default Web browser and loads the URL http: //w~.apple.com; similarly, clicking the word "Pixar" loads the URL http://www, pi xar. com.
Finally, we can attach wired actions to items in a Flash track. Flash is a file format developed by Macromedia, Inc., for displaying vector-based graphics and animations. It's especially popular for Web-based content delivery, since it combines compact data size (and hence speedier downloads) with quick rendering on the client, advanced graphics manipulations, and support for mouse-based interactivity with the graphics objects. In QuickTime 4, Apple added a Flash media handler that provides support for Flash graphics, animations, and interactivity inside of QuickTime movies. In addition, QuickTime 4 added the ability to attach wired actions to elements in a Flash track, thereby supplementing the native Flash interactivity. So, for instance, we could use Flash graphic elements (instead of sprites) to provide a user interface for a QuickTime movie. Unfortunately, we don't yet know enough about the structure of QuickTime VR movies or Flash tracks to know where to insert event atoms. As a result, we'll have to postpone our hands-on work with wiring VR or Flash content. But we do know how text tracks are put together (see Chapter 10, "Word Is Out"), so here we'll take a look at how to add some interactivity to our text tracks. Our sample application for this chapter is called QTWiredActions. The Test menu of QTWiredActions is shown in Figure 18.3. The first menu item creates the wired text movie shown in Figure 18.2. The second menu item creates the memory display movie shown in Figure 18.1. The next two menu items create two more sprite movies, which use wired actions and operands to make sprites react "physically" with the movie rectangle and with one another.
534
Chapter 18 Back In Action
Figure 18.1
A text track displaying the application's free memory.
Figure 18.2 A text track with hypertext links.
Figure 18.3 The Test menu of QTWiredActions.
Text Actions Let's begin by investigating a few of the w a y s in w h i c h w e can use w i r e d actions in text tracks. In general, w e attach an event a t o m to a text m e d i a sample by concatenating the a t o m container that holds the event a t o m onto
Text Actions
535
the media sample data. {We'll see exactly how this works in a moment.} The event atom can specify any of the kinds of events that we've considered so far, including mouse-click events, mouse-over events, frame-loaded events, and idle events. These wired text samples work exactly as you'd expect; for instance, if a text sample contains a mouse-click event atom, then the associated actions are triggered w h e n the user clicks the mouse button while the cursor is within the bounds of the text track. Sometimes, however, we'd like to handle user events that involve only part of the text displayed in a text sample. Consider once again the text movie shown in Figure 18.2. In this case, we want to trigger actions only w h e n the user clicks specific portions of the text data (namely, the strings "Apple" and "Pixar"). To handle this kind of wiring, QuickTime supports a class of atoms called hypertext atoms. The goal here is to provide the sort of hyperlinks that you typically find in Web browsers or other HTML-based applications. By clicking a hypertext link in a text track, the user can launch the default Web browser and navigate to a specific URL, by triggering the kActionGoToUaL action. But in fact hypertext atoms can contain any kind of wired actions, not just the kActionGoToURL action. So the user's actions can just as easily (for instance} trigger a jump forward or backward in the movie or cause some changes in an external movie. In the movie shown in Figure 18.2, QuickTime automatically sets the color of hypertext links to the familiar browser-default blue and underlines the hypertext. Both of these provide visual cues that the user can interact with that segment of text. QuickTime also provides wired actions that allow us to change the color of a segment of hypertext, for example, w h e n the user rolls the cursor over that segment or after the user has clicked the link. Figure 18.4 shows the same hypertext movie [this time on Windows) with the cursor resting over the first hyperlink.
A d d i n g Actions to a T e x t Sample A text media sample consists of a 16-bit length word followed by the text of that sample. Optionally, one or more atoms of additional data (called text atom extensions) may follow the text in the sample. The length word specifies the total number of bytes in the text {not including the 2 bytes occupied by the length field itself or the length of any of the optional text atom extensions). The text atom extensions are organized as "classic" atom structures: a 32-bit length field, followed by a 32-bit type field, followed by the data in the atom. Here, the length field specifies the total length of the atom [that is, 8 plus the number of bytes in the data). All the data in a text extension atom must be in big-endian format. Whether we use a hypertext atom or any of the standard types of event atoms, we add the event atom to a text media sample in the same way, by
S3G Chapter 18 Back In Action
Figure 18.4
A selected hypertext link.
1103
A traditional movie, whether stored on film, laser disk, or tape, is a continuous stream of data. A QuickTime movie can be similarly constructed, but it need not be: a QuickTime
Number of bytes of text
Text
Size Type Atom container
Figure 18.5
Text atom extension
The structure of a wired text media sample.
attaching the atom container that contains the event and action atoms to the text media sample as a text atom extension. Figure 18.5 shows the general structure of a text media sample that contains a text action. If we're given a text media sample and an event atom container, it's quite easy to wire that atom container to the text. We simply need to determine the lengths of the media sample and the atom container, enlarge the text
Text Actions
537
media sample to hold all of its existing data and the atom container, plus the 8-byte atom header shown in Figure 18.5. We set up the atom header as appropriate and then copy it and the atom container into the enlarged media sample. The size of a text atom extension for wired actions is the size of the atom container plus the size of the atom header (8 bytes). The type of the text atom extension can be one of three values. If the atom container holds a frame-loaded event atom, then the type of the text atom extension should be set to kQTEventFrameLoaded. If the atom container holds any other standard event atom (including an idle event atom), then the type of the text atom extension should be set to kQTEventType. Finally, if the atom container holds a hypertext event atom, then the type of the text atom extension should be set to 'htxt'. Currently there is no symbolic constant for this value defined in any of the public QuickTime header files, so I've included this definition in the file OTWiredActions, h: #define kHyperTextTextAtomType
FOUR CHAR CODE( ' h t x t ' ) E
Listing 18.1 shows our definition of the function QTWired AddActionsToSampl e, which adds an event atom container to an existing text media sample. Notice that the function parameters include the text media sample, the event atom container, and the type of atom extension. This type should be one of the three recognized types for text atom extensions that specify wired actions.
Listing 18.1 Adding wired actions to a text media sample. static OSErr QTWired_AddActionsToSample (Handle theSample, QTAtomContainer theActions, SInt32 theAtomExtType) Ptr long long long OSErr
myPtr = NULL; myHandleLength; myContai nerLength; myNewLength; myErr = noErr;
i f ((theSample == NULL) II (theActions == NULL)) return (paramErr) ; myHandleLength = GetHandleSize(theSample) ; myContainerLength = GetHandleSize((Handle)theActions) ; myNewLength = (long)(sizeof(long) + sizeof(OSType) + myContainerLength);
538
Chapter 18 Back In Action
SetHandleSize(theSample, (myHandleLength + myNewLength)) ; myErr = MemError() ; i f (myErr ! = noErr) goto bai I ; HLock(theSampl e) ; / / get a pointer to the beginning of the new block of space added to the sample / / by the previous call to SetHandleSize; we need to format that space as a text / / atom extension myPtr = *theSample + myHandleLength; / / set the length of the text atom extension *(long *)myPtr = EndianS32_NtoB((Iong) (sizeof(long) + sizeof(OSType) + myContai nerLength) ) ; myPtr += (sizeof(long)); / / set the type of the text atom extension *(OSType *)myPtr = EndianS32 NtoB(theAtomExtType); myPtr += (sizeof(OSType)) ; / / set the data of the text atom extension; / / we assume that this data is already in big-endian format HLock( (Handl e)theActions) ; BlockMove(*theActions, myPtr, myContainerLength); HUnlock ( (Handl e) theActi ons) ; HUnlock ( theSampl e) ; bail: return (myErr) ;
}
For more information on working with atoms, see Chapter 8, "The Atomic Caf6."
Creating Text Actions As we've just seen, we add a wired action event handler to a text sample by adding a text atom extension of type kQTEventFrameLoaded or kQTEventType to the end of the sample; the data in the text atom extension is the atom container that holds the information about the wired actions triggered by some event. So, our task boils d o w n to this: find the data in a text sample, create
Text Actions 539
an atom container holding information about the desired actions, and then append a text extension atom whose data is that atom container to the end of the text sample data. Then we replace the previous text sample with the new one in the text track. Listing 18.2 shows the code that handles the Make Memory Display Movie menu item.
Listing
18.2 Handling the Make Memory Display Movie menu item.
case IDM MAKE MEM DISPLAY MOVIE. myPrompt = QTUti I s_ConvertCToPascal St r i ng (kMDSavePrompt) ; myName = QTUti I s ConvertCToPascalString(kMDSaveFi leName) ; / / e l i c i t a f i l e from the user to save the new movie into QTFrame_PutFile(myPrompt, myName, &myFile, &myIsSelected, &mylsReplacing) ; / / create a text movie that displays the amount of memory free in the application heap i f (mylsSelected) { myErr = QTWired CreateMemoryDisplayMovie(&myFile) ; i f (myErr == noErr) QTWired AddActions ToTextMovi e (&myFi I e, theMenuItem) ;
}
mylsHandled = true; break; We don't need to consider the function QTWired_CreateMemoryDisplayMovie
in detail, since it's really just a variant of the QTText AddTextTrack function we considered in an earlier chapter. In the present case, we set the initial text in the text track to the single character "0", and we set the font to 48point Times Roman (using the constant kFontIDTimes). The interesting work in Listing 18.2 is done by the QTWired_AddActionsToTextMovie function, which creates the appropriate wired action atom container and then calls QTWired_AddActionsToSample (defined in Listing 18.1) to append that container to the first {and only) text media sample in the file created by QTWired_CreateMemoryDisplayMovie. Listing 18.3 shows our definition of QTWired AddActionsToTextMovie.
Listing
18.3 Adding wired actions to a text movie.
OSErr QTWired_AddActionsToTextMovie (FSSpec *theFSSpec, UInt16 theMenultem)
{
short short Movie
540
Chapter 18 Back In Action
myResID = O; myResRefNum = -1; myMovie = NULL;
Track Media TimeVal ue TimeVaI ue Ti meVaI ue TimeVal ue TimeVaI ue TextDescriptionHandle Handle short Fixed QTAtomContainer OSErr
myTrack = NULL; myMedia = NULL; myTrackOffset; myMedi aTime; mySampI eDurat i on; mySel ecti onDurati on; myNewMedi aTime; myTextDesc= NULL; mySample = NULL; mySampleFl ags; myTrackEdi tRate; myActions = NULL; myErr = noErr;
/ / open the movie f i l e for reading and writing myErr = OpenMovieFile(theFSSpec, &myResRefNum, fsRdWrPerm) ; i f (myErr ! = noErr) goto bai I ; myErr = NewMovieFromFile(&myMovie, myResRefNum, &myResID, NULL, newMovieActive, NULL); i f (myErr != noErr) goto bai I ; / / find f i r s t text track in the movie myTrack = GetMovielndTrackType(myMovie, klndexOne, TextMediaType, movieTrackMediaType) ; i f (myTrack == NULL) goto bai I ; / / get f i r s t media sample in the text track myMedia = GetTrackMedia(myTrack) ; i f (myMedia == NULL) goto bai I ; myTrackOffset = GetTrackOffset(myTrack) ; myMediaTime = TrackTimeToMediaTime(myTrackOffset, myTrack) ; / / allocate some storage to hold the sample description for the text track myTextDesc = (TextDescriptionHandle)NewHandle(4) ; i f (myTextDesc == NULL) goto bai I ; mySample = NewHandle(O) ; i f (mySample : : NULL) goto bai I ;
Text Actions 541
myErr = GetMediaSample(myMedia, mySample, O, NULL, myMediaTime, NULL, &mySampleDuration, (SampleDescriptionHandle)myTextDesc, NULL, 1, NULL, &mySampleFlags); i f (myErr l= noErr) goto bai I ; / / add actions to the f i r s t media sample switch (theMenultem) { case IDM MAKE HYPERTEXTMOVIE. / / create an action container for hypertext actions myErr = QTWired_CreateHyperTextAct i onContai ner (&myActions) ; i f (myErr i= noErr) goto bai I ; I
m
m
/ / add hypertext actions to sample myErr = QTWired_AddActionsToSample(mySample, myActions, kHyperTextTextAtomType) ; i f (myErr ! = noErr) goto bai I ; break; case IDM MAKE MEM DISPLAY MOVIE/ / create an action container for wired actions myErr = QTWired_CreateMemoryDisp I ayAct i onContai ner (&myActions) ; i f (myErr l= noErr) goto bai I ; / / add actions to sample myErr = QTWired_AddActionsToSample(mySample, myActions, kQTEventType) ; i f (myErr != noErr) goto bai I ; break;
}
default. myErr = paramErr; goto bai I ;
/ / replace sample in media myTrackEditRate = GetTrackEditRate(myTrack, myTrackOffset) ; i f (GetMoviesError() ! = noErr) goto bai I ; GetTrackNextInterestingTime(myTrack, nextTimeMediaSample I nextTimeEdgeOK, myTrackOffset, fixed1, NULL, &mySelectionDuration); i f (GetMoviesError() I= noErr) goto bai I ;
542
Chapter 18 Back In Action
myErr = DeleteTrackSegment(myTrack, myTrackOffset, mySelectionDuration); i f (myErr ! = noErr) goto bai I ; myErr = Begi nMedi aEdi ts (myMedia) ; i f (myErr ! = noErr) goto bai I ; myErr = AddMediaSample(myMedia, mySample, O, GetHandl eSi ze (mySample), mySampleDurat i on, (SampI eDescri pt i onHandl e)myText Desc, 1, mySampleFl ags, &myNewMedi aTi me) ; i f (myErr ! = noErr) goto bai I ; myErr = EndMediaEdits(myMedia) ; i f (myErr ! = noErr) goto bai I ; / / add the media to the track myErr = InsertMedialntoTrack(myTrack, myTrackOffset, myNewMediaTime, mySelectionDuration, myTrackEditRate) ; i f (myErr ! = noErr) goto bai I ; / / update the movie resource myErr = UpdateMovieResource(myMovie, myResRefNum, myResID, NULL); i f (myErr ! = noErr) goto bai I ; / / close the movie f i l e myErr = CloseMovieFile(myResRefNum) ; bail: i f (myActi ons ! = NULL) QTDi sposeAtomContai ner (myActi ons) ; i f (mySample ! = NULL) Di sposeHandl e (mySample) ; if
(myTextDesc ! = NULL) Di sposeHandl e ( (Handl e) myTextDesc) ;
Text Actions 543
i f (myMovie ! = NULL) Di sposeMovie (myMovie) ; return (myErr) ;
Everything in Listing 18.3 is standard Movie Toolbox stuff that we've seen before, except of course for the application functions QTWired_CreateHyperTextActionContainer (which we'll discuss in the next section) and QTWired CreateMemoryDi spl ayActionContainer. This latter function is in fact relatively simple. It builds an atom container that uses the kOperandFreeMemory operand to retrieve the amount of memory that is currently free in the application heap, as well as the kActionTextTrackPasteText wired action to paste the value returned by that operand into the text track. The kActionTextTrackPasteText action, which is new in QuickTime 5, takes three parameters: the text to paste and the beginning and ending locations in the text track to paste the text. Each time we paste a new value into the text track of the memorydisplay movie, we want to replace all of the existing text, so we'll set the second and third parameters to 0 and Oxffff, respectively. There's one undocumented "gotcha" here: in order for kActionTextTrackPasteText to have any effect, we need to explicitly enable text editing on the target text track. We can do this by executing the kActionTextTrackSetEditabl e wired action (also new to QuickTime 5). This action takes one parameter, which specifies the kind of editing we want to enable. QuickTime currently supports three settings for the editing state of a text track, which the documentation lists like this" i
#define kKeyEntryDi sabl ed #define kKeyEntryDirect #define kKeyEntryScri pt
(Once again, however, these values are not defined in any public header file, so I've added those lines to the file OTWiredActions.h.) The default value kKeyEntryOisabled indicates that no editing is allowed on the text track; key events are passed to the movie controller (which will probably ignore most of them), and any editing actions sent to the track are ignored. The value kKeyEntryDi rect indicates that direct editing is to be enabled; this means that key events are passed directly to the text track (for instance, key-down events result in the characters being inserted into the text track). The value kKeyEntryScri pt indicates that script editing is to be enabled; this means that editing actions sent to the track are interpreted by the text media handler and executed. For present purposes, we want to enable script editing on the text track. So we'll execute the code in Listing 18.4 inside of OTWired_CreateMemoryDi spl ayActi onContai ner.
5~14 Chapter 18 Back In Action
Listing 18.4
Enabling editing on a text track.
myErr = QTNewAtomContainer(theActions) ; i f (myErr ! = noErr) goto bail ; / / add an event atom that enables text editing myErr = Wi redUt i I s_AddQTEventAndAct i onAtoms (*theAct ions, kParentAtoml sContai ner, kQTEventldle, kActionTextTrackSetEditable, &myActionAtom) ; i f (myErr l= noErr) goto bai I ; myEditState = EndianSl6 NtoB(kKeyEntryScript) ; myErr = Wi redUti I s AddActi onParameterAtom(*theActi ons, myActionAtom, 1, sizeof(myEditState), &myEditState, NULL); m
On every idle event sent to the text track, we set the track to be editable Oust before we execute the kActionTextTrackPasteText action). In theory, we could enable editing just once in a frame-loaded event. But the idle event action is simpler to construct and also ensures that we can always paste the current memory value into the text track {since some other action may have disabled editing on that track). The remainder of OTWired_CreateMemoryDi spl ayActi onContai ner is straightforward. It adds an idle event call to the kActionTextTrackPasteText action,
using the result of the kOperandFreeMemory operand as the text to be pasted. QuickTime is smart enough to convert the floating-point number returned by kOperandFreeMemory into a string, as expected by kActionTextTrackPasteText. Listing 18.5 shows the complete definition. Listing 18.5
Pasting the amount of free memory into a text track.
static OSErr QTWired_CreateMemoryDi spl ayAct i onContai ner (QTAtomContainer *theAct ions)
(
QTAtom QTAtom QTAtom short UInt32 QTAtom QTAtom QTAtom OSErr
myEventAtom = O; myActionAtom = O; myParamAtom = O; myEditState; myPos; myParameterAtom = O; myExpressionAtom = O; myOperandAtom = O; myErr = noErr;
Text Actions
545
myErr = QTNewAtomContainer(theActions) ; i f (myErr ! = noErr) goto bai I ; / / add an event atom that enables text editing myErr = Wi redUti I s_AddQTEventAndActionAtoms (*theActions, kParentAtomlsContainer, kQTEventldle, kActionTextTrackSetEditable, &myActionAtom) ; i f (myErr I= noErr) goto bai I ; myEditState = EndianS16 NtoB(kKeyEntryScript) ; myErr = WiredUtils AddActionParameterAtom(*theActions, myActionAtom, I, sizeof(myEditState), &myEditState, NULL); i f (myErr I= noErr) goto bai I ; w
/ / add an event atom that displays the amount of application memory currently free myErr = Wi redUti I s_AddQTEventAndActionAtoms (*theActions, kParentAtomlsContainer, kQTEventldle, kActionTextTrackPasteText, &myActionAtom); i f (myErr l= noErr) goto bai I ; / / f i r s t parameter: the text to be pasted myErr = WiredUti I s AddActionParameterAtom(*theActions, myActionAtom, 1, O, NULL, &myParameterAtom); i f (myErr i= noErr) goto bai I ; myErr = WiredUtils AddExpressionContainerAtomType(*theActions, myParameterAtom, &myExpressionAtom) ; i f (myErr ! = noErr) goto bai I ; m
myErr = QTInsertChild(*theActions, myExpressionAtom, kOperandAtomType, 1, 1, O, NULL, &myOperandAtom); i f (myErr ! = noErr) goto bai I ; myErr = QTlnsertChild(*theActions, myOperandAtom, kOperandFreeMemory, 1, 1, O, NULL, NULL); i f (myErr ! = noErr) goto bai I ;
546
Chapter 18 Back In Action
/ / second parameter: selection range begin: 0 myPos = EndianU32_NtoB(O) ; myErr = Wi redUt i I s_AddActi onParameterAtom(*theAct ions, myActi onAtom, 2, sizeof(myPos), &myPos, NULL); i f (myErr ! = noErr) goto bai I ;
/ / third parameter: selection range end: Oxffff myPos = EndianU32_NtoB(Oxffff) ; myErr = Wi redUt i I s_AddActi onParameterAtom(*theAct ions, myActi onAtom, 3, sizeof(myPos), &myPos, NULL); bail: return (myErr) ;
}
On classic Macintosh systems {that is, Mac OS 8 and 9), the movie created by all this code will display the amount of free memory in the application heap (or essentially what you could determine by executing the Memory Manager function FreeMem). On Windows systems and on Mac OS X, where there are no application heaps in the classic Mac sense, the movie will display less useful numbers. In fact, I'm not exactly sure what the numbers displayed on Windows and Mac OS X represent.
Creating Hypertext Actions Let's consider now how to construct text movies that contain hypertext actions, like the one shown in Figures 18.2 and 18.4. The basic ideas are the same as with constructing wired text movies, except that now we add a text atom extension of type kHyperTextTextAtomType to the end of the text sample. Also, the atom container that's inside of the text atom extension should have the structure shown in Figure 18.6. As you can see, the root atom in a hypertext text atom is of type 'wtxt'. This atom contains one child of type 'htxt' for each hypertext link in the text sample. Let's call this child atom a hypertext item atom. A hypertext item atom specifies the information about a single hypertext link in a text sample. We need to specify the starting point and ending point in the sample text for the hypertext link, and we need to specify the event and action atoms associated with that segment of text. So a hypertext item atom needs to contain at least three children, of types ' s t r t ' , 'end ', and kQTEventType. We'll use these constants:
Text Actions 547
#define #define #define #define #define
kHyperTextTextAtomType kTextWiredObjectsAtomType kHyperTextltemAtomType kRangeStart kRangeEnd
FOURCHARCODE('htxt') FOURCHARCODE('wtxt') FOURCHARCODE( 'htxt' ) FOURCHARCODE('strt') FOURCHARCODE('end ') D
D
There's no need to consider the function QTWired CreateHyperTextActionContainer in detail. It simply builds the atom container shown in Figure 18.6, using bard-coded values for the parameter data of the kRangeStart and kRangeEnd atoms. Each of our hypertext item atoms contains three event atoms. The first event atom looks for mouse dicks on the hypertext link and executes a kActionGoToURL action in response. The second and third event atoms watch for the cursor to enter and exit the hypertext link, changing the color of the text appropriately. Listing 18.6 shows the code for changing the hypertext color to purple when the mouse enters the first byperlink. The kActionTextTrackSetHyperTextColor action takes two parameters, which are the index of the hypertext link whose color is to change and the desired new m
color.
wtxt 1 htxt 1 I strt 1 Start position
I strt i Start position
l
end 1 End position
l
kQTEventType Event type Action atoms
Figure 18.6
548
I
The structure of a hypertext text atom.
Chapter 18 Back In Action
end 1 End position
Listing 18.6 Changing the color of a hypertext link. Modi f i erTrackGraphi csModeRecord
myGraphi c sModeRecord;
myErr = Wi redUt i I s_AddQTEventAndAct i onAtoms (*theAct ions, myHyperTextAtom, kQTEventMouseEnter, kActionTextTrackSetHyperTextColor, &myActionAtom) ; i f (myErr ! = noErr) goto bai I ; mylndex = EndianS16 NtoB(1); myErr = Wi redUtil s AddActionParameterAtom(*theActions, myActionAtom, kIndexOne, sizeof(mylndex), &myIndex, NULL); i f (myErr ! = noErr) goto bai I ; u
myGraphi csModeRecord, graphi csMode = EndianS32_NtoB(di therCopy) ; myGraphi csModeRecord, opCoI or. red = Endi anS16_NtoB (0x9999) ; myGraphi csModeRecord, opCoI or. green = EndianS16_NtoB (OxO000) ; myGraphi csModeRecord, opCoI or. bI ue = Endi anS16_NtoB (Oxcccc) ; myErr = WiredUtils AddActionParameterAtom(*theActions, myActionAtom, kIndexTwo, s i zeof (myGraphi csModeRecord), &myGraphicsModeRecord, NULL);
Take a look at the source file QTWiredActions, c for the complete definition of QTWired_CreateHyperTextActi onContai ner.
Key Events QuickTime 5 introduced a new type of event, the key event (of type kQTEventKey), which is issued when keys on the keyboard are pressed. Consider, for instance, the movie shown in Figure 18.7, which displays the ASCII character code of whatever key the user presses. In the case shown here, the user has just pressed the "T" key. A key event can be used in event atoms just like any of the other event types we've encountered so far. What's interesting about key events is that
Figure 18.7 A movie that displays ASCII character codes of pressed keys.
Key Events 549
we can determine not only that the user has pressed a key, but also which particular key was pressed. We do this by inspecting the event parameters of the key event, using the operand k0perandEventParameter. This operand itself takes one parameter, which specifies the index of the event parameter we want to inspect. In the case of key events, we can inspect any one of five event parameters: 1. Parameter index 1 is the horizontal position of the cursor at the time the key event occurred, in coordinates relative to the text track rectangle. 2. Parameter index 2 is the vertical position of the cursor at the time the key event occurred, in coordinates relative to the text track rectangle. 3. Parameter index 3 encodes any modifier keys that are down when the key event occurs, using these constants from Events.h: enum { cmdKey shiftKey alphaLock optionKey controIKey
= 1 << cmdKeyBit, = 1 << s h i f t K e y B i t , = i << alphaLockBit,
/ / Ox0100 / / Ox0200 / / Ox0400
= 1 << optionKeyBit, = 1 << controIKeyBit
/ / Ox0800 / / Ox1000
}; For instance, if the Control and Shift keys are both down when the key event occurs, then the third parameter would be 0x00001200, or 4608. 4. Parameter index 4 is the ASCII character code of the key pressed. 5. Parameter index 5 is the virtual key code {or scan code) of the key pressed. A virtual key code is a value that represents a specific physical key on a specific model of keyboard. As a result, these values are generally less useful than the ASCII character codes. We can construct the ASCII-display movie in Figure 18.7 in exactly the same way we previously constructed the memory-display movie. Instead of issuing the kActionTe• action in response to idle events, we now do so in response to key events. And we get the text to be pasted not from the k0perandFreeMemory operand, but from the k0perandEventParameter operand. Listing 18.7 shows some of the code we could use to construct the ASCII-display movie. (This code could be substituted for part of Listing 18.5, for instance.)
550
Chapter 18 Back In Action
Listing 18.7 Adding a key event. QTAtom short
myEventParamAtom= O; myIndex;
/ / add an event atom that displays the ASCII code of the pressed key myErr = Wi redUt i I s_AddQTEventAndActi onAtoms (*theAct i ons, kParentAtoml sContai ner, kQTEventKey, kActionTextTrackPasteText, &myActionAtom); i f (myErr l= noErr) goto bai I ; / / f i r s t parameter: the text to be pasted myErr = WiredUtils AddActionParameterAtom(*theActions, myActionAtom, 1, O, NULL, &myParameterAtom) ; i f (myErr i= noErr) goto bai I ; myErr = Wi redUti I s AddExpressionContainerAtomType(*theActions, myParameterAtom, &myExpressi onAtom) ; i f (myErr l= noErr) goto bai I ; myErr = QTInsertChild(*theActions, myExpressionAtom, kOperandAtomType, 1, 1, O, NULL, &myOperandAtom) ; i f (myErr i= noErr) goto bai I ; myErr = QTInsertChild(*theActions, myOperandAtom, kOperandEventParameter, 1, 1, O, NULL, &myEventParamAtom) ; i f (myErr l= noErr) goto bai I ; mylndex = EndianS16 NtoB(4); myErr = QTInsertChild(*theActions, myEventParamAtom, kActionParameter, 1, 1, sizeof(mylndex), &mylndex, NULL);
Notice that we add a parameter atom (myEventParamAtom) to the operand atom to specify the desired index for the event p a r a m e t e r we want to retrieve. In this case, we specify the index 4 to obtain the ASCII character code of the key pressed. It's also possible to use the k0perandEventParameter operand to retrieve event parameters for mouse-related events, such as mouse-enter and mouseclick events. With mouse events, there are only three available parameters, which are the same as the first three parameters of key events: the horizontal and vertical mouse positions, and the modifier keys currently pressed.
Key Events 551
You may recall that in Chapter 16, "Wired," we used the kOperandMouseLocal HLoc and kOperandMouseLocal VLoc operands to get these mouse positions. In QuickTime 5 and later, we can instead use kOperandEventParameter, if we so desire.
Bouncing Sprites In several of the previous chapters, we've seen that we can move a sprite around inside a sprite track by altering its matrix, either directly (by setting the sprite's matrix property) or indirectly (by performing wired actions such as kActionSpriteTranslate). In this section and the next, we'll consider a couple of ways to build on this capability. First, we'll see how to make a sprite bounce around inside of the sprite track's enclosing rectangle; later we'll see how to figure out w h e n two sprites collide with one another. Both of these are ways to give a sprite "physical" properties so that it appears to react with things around it. Figure 18.8 shows (about as well as any static image can, I guess) a sprite bouncing off the track's enclosing rectangle. The sprite is first moving down and to the right; w h e n its bottom edge touches the bottom of the track rectangle, the sprite bounces up and continues to the right. When any other edge of the sprite touches a side of the track rectangle, the sprite does the appropriate thing by moving back away from the side it touched but otherwise continuing in the same direction. (In the event that two of its edges touch two sides of the track rectangle at the same time, the sprite would reverse both its horizontal and vertical directions; this would happen w h e n the sprite moves cleanly into a corner of the track rectangle.)
Figure 18.8 A sprite bouncing off the movie edge.
552
Chapter 18 Back In Action
Moving the Sprite The first thing we need to do is get the sprite moving. To do this, we can simply change the horizontal and vertical positions of the sprite during idle events. Since we're going to be changing the direction of movement w h e n the sprite collides with the track rectangle, we'll maintain two sprite track variables whose values are the number of pixels in the horizontal and vertical direction that the sprite is to be offset during the next idle event. We'll use these variable IDs: #define kXMoveVarID #define kYMoveVarID
2000 2100
We can set the initial horizontal and vertical offsets by adding a couple of frame-loaded event actions to the sprite, using our utility WiredUtils_AddSpriteTrackSetVariableAction (defined in Chapter 16, "Wired")" #define kldleOffset
2
myErr = WiredUti I s_AddSpri teTrackSetVari abl eActi on (mySample, kParentAtomlsContainer, kQTEventFrameLoaded, kXMoveVarID, kldleOffset, O, NULL, 0); myErr = WiredUt i I s_AddSpri teTrackSet Vari abl eAct i on (mySample, kParentAtomlsContainer, kQTEventFrameLoaded, kYMoveVarID, kldleOffset, O, NULL, 0);
During idle events, we'll move the sprite horizontally by the current value of the kXMoveVarID variable and vertically by the current value of the kYMoveVarID variable. The speed of the moving sprite is determined both by the values of these variables and by the frequency with which we receive idle events (which, you'll recall, is determined by the sprite track's kSpri teTrackPropertyOTIdleEventsFrequency property). For the bouncing sprite movie, we'll tell the sprite media handler to send us an idle event every tick, and (as you can see) we'll offset the sprite in each direction by two pixels during every idle event. We add actions to move the sprite during an idle event like this: myErr = WiredUti I s_AddQTEventAtom(mySpri teData, kParentAtoml sContai ner, kQTEventldle, &myEventAtom); myErr = QTWired AddTransl ateByVari abl esActi on (mySpri teData, myEventAtom, kXMoveVarID, kYMoveVarID, NULL); D
Bouncing Sprites 553
The QTWired_AddTranslateByVariablesAction function adds to the specified event atom (here, myEventAtom) an action atom that translates the sprite
relatively, taking the horizontal and vertical offsets from the sprite track variables whose IDs are specified by the third and fourth parameters (here, kXMoveVarID and kYMoveVarID). Listing 18.8 shows our definition of the QTWired AddTrans 1ateByVari abl esAct i on function. m
Listing 18.8 Translating a sprite using variable values. static OSErr QTWired AddTranslateByVariablesAction (QTAtomContainer theSprite, QTAtom theParentAtom, QTAtomID theXVariableID, QTAtomID theYVariableID, QTAtom *theActi onAtom) QTAtom QTAtom QTAtom QTAtom QTAtom QTAtom QTAtomID Boolean OSErr
myActionAtom = O; myExpressionAtom = O; myParamAtom = O; myOperatorAtom = O; myOperandAtom = O; myOperandTypeAtom = O; myVariableID; myBoolean; myErr = paramErr;
i f (theSprite == NULL) goto bai I ; / / add a translate action atom to the specified parent atom myErr = WiredUtils AddActionAtom(theSprite, theParentAtom, kActionSpriteTranslate, &myActi onAtom) ; i f (myErr ! = noErr) goto bai I ; / / f i r s t parameter: get value of variable theXVariableID myErr = WiredUtils AddActionParameterAtom(theSprite, myActionAtom, kFirstParam, O, NULL, &myParamAtom); i f (myErr ! = noErr) goto bai I ; D
myErr = WiredUt i I s_AddExpressi onContai nerAtomType(theSpri te, myParamAtom, &myExpressi onAtom) ; i f (myErr ! = noErr) goto bai I ;
554
Chapter 18 Back In Action
myErr = QTInsertChild(theSprite, myExpressionAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); i f (myErr ! = noErr) goto bai I ; myErr = QTInsertChi I d (theSpri te, myOperandAtom, kOperandSpri teTrackVari abl e, 1, 1, O, NULL, &myOperandTypeAtom); i f (myErr l= noErr) goto bai I ; myVariableID = EndianU32 NtoB(theXVariableID); myErr = QTInsertChild(theSprite, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); i f (myErr l= noErr) goto bai I ; m
/ / second parameter: get value of variable theYVariableID myErr = Wi redUti I s AddActionParameterAtom(theSprite, myActionAtom, kSecondParam, O, NULL, &myParamAtom); i f (myErr != noErr) goto bai I ; myErr = Wi redUti I s AddExpressi onContai nerAtomType (theSpri te, myParamAtom, &myExpressi onAtom) ; i f (myErr i= noErr) goto bai I ; myErr = QTInsertChild(theSprite, myExpressionAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); i f (myErr l= noErr) goto bai I ; myErr = QTInsertChild(theSprite, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, O, NULL, &myOperandTypeAtom); i f (myErr l= noErr) goto bai I ; myVariablelD = EndianU32 NtoB(theYVariablelD); myErr = QTInsertChild(theSprite, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); i f (myErr ! = noErr) goto bai I ; m
Bouncing Sprites 555
/ / third parameter: false (for relative translation) myBool ean = false; myErr = WiredUti I s AddActionParameterAtom(theSpri te, myActionAtom, kThirdParam, sizeof(myBoolean), &myBoolean, NULL); m
bail: i f (theActionAtom l= NULL) *theActionAtom = myActionAtom; return (myErr) ;
Here we add an action of type kActionSpriteTranslate, which requires three parameters: the desired horizontal translation, the desired vertical translation, and a Boolean that indicates whether the translation is absolute or relative. For the first and second parameters, we add expression container atoms that retrieve the value of the variable with the specified ID. (For more information about expression container atoms, see Chapter 16, "Wired.")
Detecting Track Rectangle Collisions So far, so good. But if this were all the wiring we attached to the sprite, we wouldn't get quite the behavior we're looking for. When the movie was first opened, the sprite would begin moving down to the right and would continue moving in that direction forever. We need to add some wiring to figure out when an edge of the sprite hits an edge of the track rectangle and then adjust the direction of movement accordingly. In a nutshell, we want to add some logic that says: 9 If the left edge of the sprite is less than the left edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the horizontal direction of travel. 9 If the right edge of the sprite is greater than the right edge of the track rectangle, t h e n move the sprite b a c k inside the track r e c t a n g l e a n d reverse the horizontal direction of travel. 9 If the top edge of the sprite is less than the top edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the vertical direction of travel. 9 If the bottom edge of the sprite is greater than the bottom edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the vertical direction of travel.
556
Chapter 18 Back In Action
It's worth pointing out that a more elegant design would use two "if-else" statements here instead of four "if" statements, since both the opposite edges of our sprite cannot be outside the track bounds at the same time. This refinement would, however, require a more complicated wiring. I'll leave that as an exercise for the persnickety reader. We know the dimensions of the sprite track (since we created it using the constants kIconSpriteTrackHeight and kIconSpriteTrackWidth). So all we really have to learn now is how to figure out the position of the sprite's edges. The sprite media handler supports these four operands, which give us the information we need: enum { kOperandSpriteBoundsLeft kOperandSpriteBoundsTop kOperandSpriteBoundsRight kOperandSpriteBoundsBottom
};
= = = =
3072, 3073, 3074, 3075
The operand kOperandSpriteBoundsLeft, for instance, returns the left side of the sprite's bounding box (the rectangle that encloses the sprite), in the local coordinate system of the sprite track. In our code, we'll add the side-bounce logic to the sprite by making four calls to a QTWiredActions function QTWired_AddSi deBounceToSpri te, like this" QTWired AddSideBounceToSprite (mySpri teData, kOperandSpriteBoundsLeft, 1, kOperatorLessThan, kXMoveVarID); QTWired AddSideBounceToSprite (mySpriteData, kOperandSpriteBoundsRight, klconSpriteTrackWidth, kOperatorGreaterThan, kXMoveVarID); QTWired AddSideBounceToSprite(mySpri teData, kOperandSpriteBoundsTop, 1, kOperatorLessThan, kYMoveVarID); QTWired AddSideBounceToSprite(mySpri teData, kOperandSpriteBoundsBottom, kIconSpriteTrackHeight, kOperatorGreaterThan, kYMoveVarID);
The QTWired_AddSideBounceToSpri te function is really quite simple, but (as we've grown to expect w h e n building wired actions) a tad lengthy. Since we've built a few wired event handlers already, let's just survey the main points. First of all, we want to install an idle event atom by using our utility function WiredUtils_AddQTEventAndActionAtoms. The action should be a kActionCase action, since we want to ask whether a side of the sprite lies outside the track rectangle. As we saw in the previous chapter, a kActionCase action has a single parameter, whose data is an atom of type kConditionalAtomType. This conditional atom, in turn, has two children, an expression container atom and an action list atom.
Bouncing Sprites 557
The expression container atom needs to test w h e t h e r the specified side of the sprite lies outside the sprite track rectangle. As you can see, w h e n we call QTWired AddSideBounceToSpri te, we pass in the operand that we need to use to select the side, along with the track limit and the test to perform {for instance, kOperatorLessThan). We build the expression container atom data like this: WiredUti I s_AddOperatorAtom(theSpri te, myExpressionAtom, theTest, &myOperatorAtom) ; / / f i r s t operand" the specified side of the sprite QTInsertChild(theSprite, myOperatorAtom, kOperandAtomType, 1, 1, O, NULL, &myOperandAtom) ; QTInsertChild(theSprite, myOperandAtom, theSide, 1, 1, O, NULL, NULL); / / second operand" the specified limit WiredUti I s_AddOperandAtom(theSprite, myOperatorAtom, kOperandConstant, 2, NULL, theLimit);
The contents of the action list atom are pretty straightforward. R e m e m b e r that we need to perform two actions" translate the sprite back to the edge of the track rectangle, and reverse the direction of travel in the horizontal or vertical dimension. We've already seen how to construct a translate action, so we don't need to repeat that here. We can change the direction of travel simply by negating the value of the variable whose ID is passed to QTWired AddSideBounceToSprite. We'll do that using another function, QTWired_AddNegateVariableAction, cleaned in Listing 18.9.
Listing
18.9
Negating a sprite track variable.
static OSErr QTWired_AddNegateVariableAction (QTAtomContainer theSprite, QTAtom theParentAtom, QTAtomID theVariableID)
{
QTAtom QTAtom QTAtom QTAtom QTAtom QTAtom QTAtomID OSErr if
558
myActionAtom = O; myExpressionAtom = O; myParamAtom = O; myOperatorAtom = O; myOperandAtom = O; myOperandTypeAtom = O; myVariableID; myErr = paramErr;
((theSprite =: NULL) goto bai I ;
Chapter 18 Back In Action
II
(theParentAtom : : 0))
myErr = WiredUtils AddActionAtom(theSprite, theParentAtom, kActionSpriteTrackSetVariable, &myActionAtom) ; i f (myErr ! = noErr) goto bai I ; / / add parameters to the set variable action: variable ID (QTAtomID) and value ( f l o a t ) myVariableID = EndianU32 NtoB(theVariableID); myErr = QTInsertChild(theSprite, myActionAtom, kActionParameter, O, (short)kFirstParam, sizeof(myVariableID), &myVariableID, NULL); i f (myErr ! = noErr) goto bai I ; myErr = QTInsertChild(theSprite, myActionAtom, kActionParameter, O, (short) kSecondParam, O, NULL, &myParamAtom); i f (myErr != noErr) goto bai I ; myErr = Wi redUti I s_AddExpressionContainerAtomType(theSpri te, myParamAtom, &myExpressi onAtom) ; i f (myErr ! = noErr) goto bai I ; myErr = QTInsertChild(theSprite, myExpressionAtom, kOperatorAtomType, kOperatorNegate, 1, O, NULL, &myOperatorAtom); i f (myErr ! = noErr) goto bai I ; myErr = QTInsertChild(theSprite, myOperatorAtom, kOperandAtomType, O, 1, O, NULL, &myOperandAtom); i f (myErr l= noErr) goto bai I ; myErr = QTInsertChild(theSprite, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, O, NULL, &myOperandTypeAtom); i f (myErr ! = noErr) goto bai I ; myVariableID = EndianU32 NtoB(theVariableID); myErr = QTInsertChild(theSprite, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); u
bai I 9 return (myErr) ;
}
Bouncing Sprites 559
There's nothing too exciting here; this just says: set the value of the variable whose ID is theVariableID to the result of negating the value of the variable whose ID is theVariableID.
Colliding Sprites So now we've got sprites bouncing off the walls. Let's get them to bounce off one another as well. Specifically, let's create a movie with two sprites, both of which have the bouncing logic that we developed in the previous section. Then, let's add some wiring to make them recoil from one another w h e n part of one sprite touches part of the other. Figure 18.9 shows two sprites about to collide: the sprite with the new QuickTime logo has just bounced off the bottom and is moving up to the right; the other sprite is moving down to the left. When the sprites collide, we'll reverse the horizontal direction of travel of each sprite unless they are traveling in the same horizontal direction when they collide. And, ditto for the vertical directions of travel. This logic gives the sprites a nice feel as they bounce around. But how do we know w h e n two sprites collide? Well, we know the coordinates of each of the corners of a sprite [using the operands we encountered in the previous section). So a reasonable strategy might be to ask, for each corner of the sprite, whether it lies on top of the other sprite. QuickTime 5 introduced a very useful operand for this, the k0perandSpriteTrackSpriteIOAtPoint operand. This operand takes two parameters, which are the horizontal and vertical coordinates of a point; it returns the ID of the topmost sprite at that point, if any. In the case where we have just two sprites that can collide, we need to .ff:arh
fhp
r, n l l l e i n n
Figure 18.9 Two sprites colliding.
SGO Chapter 18 Back In Action
Inalr
fn nnlv
nnp
nf fhp
enrlfpe
Th.f
enrlf~
I l p f ' e r~11 if
the collider) can check, on every idle event, whether any os its four corners has come into contact with any part of the other sprite. That is, we'll call kOperandSpriteTrackSpriteIDAtPoint four times, each time passing one of the four corners of the collider. QTWiredActions uses the QTWired AddCollisionLogicToSprite function (defined in Listing 18.10) to attach the collision logic to the collider.
Listing 18.10 Wiring a sprite for collisions. OSErr QTWired_AddCollisionLogicToSprite (QTAtomContainer theSprite)
(
OSErr
myErr = noErr;
myErr = QTWired_AddCornerCol I i s i onLogi cToSpri te (theSpri te, kOperandSpriteBoundsLeft, kOperandSpri teBoundsTop) ; i f (myErr l= noErr) goto bai I ; myErr = QTWired_AddCornerCol I i sionLogi cToSpri te(theSpri te, kOperandSpriteBoundsRight, kOperandSpri teBoundsTop) ; i f (myErr ! = noErr) goto bai I ; myErr = QTWired_AddCornerCol I i si onLogi cToSpri te (theSpri te, kOperandSpriteBoundsLeft, kOperandSpri teBoundsBottom) ; i f (myErr ! = noErr) goto bai I ; myErr = QTWired_AddCornerCol I i s i onLogi cToSpri te (theSpri te, kOperandSpriteBoundsRi ght, kOperandSpri teBoundsBottom) ; bail. return (myErr) ;
}
We won't bother to dissect QTWired_AddCornerCol I i sionLogi cToSpri te, as it mostly covers routine ground while setting a new record for length (almost 300 lines of code and comments). The key step is using kOperandSpri teTrackSpriteIDAtPoint, as just described. You should know that kOperandSpriteTrackSpriteIDAtPoint does its work by hit-testing for sprites in the sprite track (probably using SpriteMediaHitTestAlISprites, which we used in Chapter 14, "A Goofy Movie"). So a sprite ID will be returned as the operand's value only if some nontransparent part of a sprite is situated at the specified point.
Colliding Sprites 561
Our collision logic as developed so far is pretty good, but alas not perfect. Recall that we are testing each of the four corners of the collider sprite's bounding rectangle to see if it lies on top of some sprite. If the collider's sprite image at that corner is nontransparent, then the call to k0perandSpriteTrackSpritelDAtPoint will always return a nonzero value (since the sprite hit test will find that corner, even if the other sprite is nowhere near the collider). We can solve this problem by ensuring that the other sprite has a lower layer property than the collider (so that we get its ID is the other sprite and the collider overlap at the tested point) and by ignoring the collider's ID if k0perandSpriteTrackSpriteIDAtPoint returns it to us. Another problem arises if the collider's sprite image is transparent at a corner whose coordinates are passed to k0perandSpri teTrackSpriteIDAtPoint. In this case, it's possible for that corner to lie on top of a nontransparent part of the other sprite and hence trigger a hit, even though the sprites do not appear to be touching. Figure 18.10 illustrates this possibility. In this case, the collider is the top-right sprite, and its lower-left corner is transparent. k0perandSpriteTrackSpriteIDAtPoint will return the ID of the other sprite because the collider's lower-left corner does in fact lie on top of a nontransparent pixel of the other sprite. I don't see an easy way to solve this problem, but all in all it's a fairly minor one. For most purposes, I suspect, it's good enough to have the sprites recoil from one another using the simple wiring we've developed here. More complex collision algorithms are of course left as exercises for the reader.
Figure 18.10 Testing a transparent corner of the collider sprite.
562
Chapter 18 Back In Action
Conclusion In this chapter, we've seen how to add wiring to text tracks, and we've investigated a few of the text-related actions and operands added in QuickTime 5. We've also touched briefly on the key event {of type kQTEventKey) and seen for the first time the technique for retrieving event parameters. Finally, we've added some wiring to allow sprites to collide with the track rectangle and with one another. We've now reached the end of our "mini-series" on sprites and wired actions. We'll revisit them in Volume Two, QuickTime Toolkit, especially when we learn how to work with QuickTime VR movies and Flash tracks.
Conclusion 563
This Page Intentionally Left Blank
Glossary
action atom
An atom (of type
kAction) that
specifies a wired action.
action flags A set of flags that determine how wired actions affect values of sprite properties or other settings. action list
A list of one or more action atoms.
action target toward.
The element of a QuickTime movie that an action is directed
add in parallel To add a text track to a movie so that the text track is positioned below the existing movie box, with a height that accommodates the pasted text.
alias data handler
See file data handler.
alternate data rate movie file A movie file that references other movies, each tailored for downloading across a network connection of a certain speed. anchor point tion. animate
The point that stays fixed during a scaling or rotation opera-
To cause a sprite to change one or more of its properties.
annotation
See movie annotation.
area of interest The part of a test image that is displayed in the standard image compression dialog box.
atom (1) A block of data preceded by an atom header. A QuickTime movie file is composed of a sequence of one or more atoms. (2) A block of data of type OTAt0m that is contained in an atom container.
565
atom container A block of memory that is structured in a hierarchical arrangement of container atoms {which contain other atoms) and leaf atoms {which contain data). An atom container is referenced by a handle of type OTAtomContai ner.
atom container atom
See atom (2).
atom header An 8-byte structure that precedes the atom data in an atom. An atom header consists of a 4-byte length value and a 4-byte type. atom offset
The position of an atom (2) in an atom container.
attach To logically connect a movie controller to its associated movie. This logical connection enforces a physical connection between the movie controller bar and the movie box. attached movie controller A movie controller component that has been attached to its associated movie. The movie controller draws the movie controller bar contiguous to and just below the associated movie. The rectangle containing the controller bar and the movie box is used in calculating the boundary rectangle of the movie controller. attached movie controller bar attached movie controller.
A movie controller bar associated with an
automatic link Any part of a hypertext reference track that, when loaded by the text media handler, launches a specified URL in the user's Web browser. Compare to clickable link. badge A user interface element that can be superimposed on a movie when the controller bar is hidden. basename The portion of a URL following the rightmost URL separator, or the portion of a pathname following the rightmost path separator. big endian A way of storing multibyte data in memory wherein the most significant byte is lowest in memory. Compare to little enclian.
byte swapping The process of exchanging selected bytes in a multibyte quantity. Often done to convert between big-endian and little-endian values. callout function
See movie callout function.
Carbon A set of programming interfaces and a runtime library that together define a subset of Macintosh Operating System and Toolbox APIs that are supported both on "classic" Mac operating systems (Mac OS 8 and 9) and on Mac OS X. Carbonize To convert an application or other software module so that it conforms to the Carbon specification.
566
Glossary
cel animation A m e t h o d of animation in which each frame of the animation is a fully rendered picture of a character or characters superimposed on a background image. chapter
One of the parts of the track associated with a chapter track.
chapter list The list of items that appear in the pop-up m e n u associated with a chapter track. chapter list item
An item in a chapter list.
chapter track A text track that has been associated with some other track [often a video or sound track); w h e n a movie contains a chapter track, the movie controller will build, display, and handle a pop-up m e n u whose m e n u items are the text in the various samples in that track. child atom An atom (2) that is contained in some other atom i2). Compare to parent atom. child movie A QuickTime movie that is contained in a parent movie, using the movie-in-movie capability introduced in QuickTime 4.1. chunk atom
See atom (1).
classic atom
See atom (1).
clickable link Any part of a hypertext reference track that, w h e n clicked, launches a specified URL in the user's Web browser. Compare to automatic link.
codec A component that provides data compression or decompression services. (Codec is an abbreviation of compressor/decompressor.) collision detection The process of determining w h e t h e r two or more objects {for instance, sprites) have collided. Compare to hit testing. compression data.
The process of reducing the size of some discrete collection of
constant operand A leaf atom of type kOperandC0nstant whose atom data is a floating-point value. container atom atoms.
control flags controller
An atom (1) or atom (2) that contains one or more other
See movie controller control flags, time base control flags.
See movie controller component.
controller bar
See movie controller bar.
Glossary 567
C string A string that consists of a number of characters terminated by a NULL byte. Compare to Pascal string. custom image progress function An image progress function that is implemented by an application or component and that replaces the default image progress function provided by the Image Compression Manager. Compare to default image progress function. custom movie progress function A movie progress function that is implemented by an application or component and that replaces the default movie progress function provided by the Movie Toolbox. Compare to default movie progress function. custom progress function movie progress function.
See custom image progress function, custom
cut movie
A movie played during a transition, as between levels of a game.
cut scene
See cut movie.
data handler A QuickTime component (of type DataHandlerType) that is responsible for reading and writing a media's data. Data handlers also provide data input and output services for other parts of QuickTime. data handler completion function An application-defined function that is executed after an asynchronous read or write operation of a data handler. data reference A handle to a block of memory that uniquely identifies the location of media data for a QuickTime movie or other data that QuickTime can manage. data reference extension A block of additional data associated with a data reference to assist QuickTime in working with the data picked out by the data reference. data reference record A structure of type DataReferenceRecord that specifies a data reference and its type. default image progress function An image progress function that is implemented by the Image Compression Manager. Compare to custom image progress function. default movie progress function A movie progress function that is implemented by the Movie Toolbox. Compare to custom movie progress function. default progress function movie progress function.
default sprite action.
568
Glossary
See default image progress function, default
The sprite that contains the event atom that triggered a given
default target
delta frame
The action or operand target used if no target is specified. See difference frame.
detach To logically separate a movie controller component from its associated movie. This logical separation permits [but does not require) subsequent physical separation between the movie controller bar and the movie box. detached movie controller A movie controller component that has been detached from its associated movie. The movie controller draws the movie controller bar in a location specified by the calling application. Only the controller bar rectangle is used in calculating the boundary rectangle of the movie controller. detached movie controller bar detached movie controller.
A movie controller bar associated with a
difference frame A frame of video that contains only the differences from the previous frame. Compare to key frame. See also override frame. direct editing A mode of interacting with a text track where key events are passed directly to the text track. Compare to script editing. disabled track A track that does not directly contribute to the overall user experience. Compare to enabled track.
display clipping region
See movie display clipping region.
display coordinate system The QuickDraw coordinate plane. double-fork movie file A O uickTime movie file in which the movie's metadata is stored in the resource fork and the movie's media data is stored in the data fork. Compare to single-fork movie file.
drop-frame timecoding A method of timecoding in which two frame num-
bers are dropped at the start of every minute, except for every minute that is evenly divisible by 10.
edit state A collection of information that describes the movie's contents, such as the sound and video data in the movie; the current edit state also includes the movie's current selection. enabled track A track that directly contributes to the overall user experience. Compare to disabled track. event atom An atom that associates an event with one or more actions that are triggered when that event occurs. event parameter One of the pieces of information about an event that can be retrieved using the k0perandEventParameter operand.
Glossary 569
explicit flags See movie controller explicit flags. export To convert a graphic or movie into a n e w format or to change the graphic or movie data in some other way.
exporter
See graphics export component, movie export component.
expression The contents of an expression container atom. extended procedure One of several callback p r o c e d u r e s that extend the basic functionality of the standard image c o m p r e s s i o n dialog c o m p o n e n t .
extended procedures structure A data structure (of t ~ e SCExtendedProcs) that specifies one or m o r e extended procedures.
external media reference
A reference to m e d i a data contained in some
other file.
external movie record A data structure (of type QTGetExternalMovieRecord) that specifies information about the target of an intermovie message or action. Fast Start movie file A Q u i c k T i m e movie file in w h i c h the movie atom is stored as one of the first atom in a single-fork movie file. file data handler
A data handler that can read and write data stored in files on a local file system [sometimes called the alias data handler or the HFS data
handler). file data reference A data reference that specifies a file on a local storage volume {or on a remote storage v o l u m e m o u n t e d on the local machine). A file data reference is an alias handle.
file filter function
A function passed to NavGetFile or StandardGetFilePreview that d e t e r m i n e s w h i c h files are displayed in the list of files displayed
in the file-opening dialog box.
filenaming extension
A filename (formatted as a Pascal string} attached to the referring data of a handle data reference.
file-opening dialog box
A dialog box displayed by a call to NavGetFile or StandardGetFilePreview that allows the user to select a file to be opened.
file preview
Information the user an idea of w h a t ' s image or a movie preview describes or represents the
file system specification file.
570 Glossary
displayed in a file-opening dialog box that gives in the file. A file p r e v i e w can be a movie poster (if the file is a movie file) or any other data that file.
A data structure (of t ~ e
FSSpec) that refers to a
Flash A file format and playback engine developed by Macromedia, Inc. for displaying vector-based graphics and animations. flattened movie file free atom Time.
See self-contained movie file.
An atom (of type FreeAtomType) whose data is ignored by Quick-
function operand An operand that returns information about some object, most often the current setting of some property of the operand's target. graphic
A still image.
graphics export component A QuickTime component (of type 'grex') that provides services for saving a still image in a new format. graphics exporter
See graphics export component.
graphics import component A QuickTime component (of type 'grip') that provides services for handling still images, such as reading them from files and drawing them in windows on the screen. graphics importer
See graphics import component.
handle data handler A data handler that can read and write data stored in memory that is accessed using a handle. handle data reference A data reference that specifies a block of memory data accessed using a handle. A handle data reference is a handle to a 4-byte block of memory that holds a handle to some other block of data.
HFS data handler
See file data handler.
hit testing The process of determining whether the user has clicked on an object (for instance, a sprite). Compare to collision detection. hook function A function that is called to handle user selection of items in the standard image compression dialog box.
HREF track See hypertext reference track. hypertext atom An atom that attaches hypertext and other wired capabilities to text in a text track. hypertext item atom An atom that specifies information about a single hypertext link in a text sample. hypertext reference track A text track that has a special name (to wit, HREFTrack) and contains some media samples that pick out URL links. identity matrix The matrix that transforms an image into the identical image; in other words, the identity matrix leaves an image unchanged.
Glossary 571
ICM See Image Compression Manager. ICM completion procedure A procedure that is called w h e n an asynchronous compression operation has completed. See also Image Compression Manager.
ICM completion procedure record A data structure (of type ICHCompletionProcRecord) that specifies an Image Compression Manager (ICM) completion procedure.
idling See tasking. Image Compression Manager (ICM) The part of QuickTime that provides services for compressing and decompressing images and sequences of images. image description A data structure that contains information about an image.
image matrix See image transformation matrix. image progress dialog box A dialog box that is displayed and updated by an image progress function.
image progress function A function that the Image Compression Manager calls when an operation (such as importing or exporting an image) promises to take a significant amount of time. Compare to custom image progress
function, default image progressfunction.
image transformation matrix A 3-by-3 array of numbers that specifies how to map an image from one two-dimensional coordinate space into another two-dimensional coordinate space.
import To convert a graphic or other data into a form that can be opened and displayed by QuickTime. importer See graphics import component, movie import component. importing in place The process of importing a movie file without having to make a copy of the file data. input map A data structure attached to a track's media that specifies how the track should interpret any data being sent to it from a modifier track. Inside Macintosh The original six-book set of documentation for the Macintosh operating system or the later multibook revision of that documentation.
interleaving The process of organizing data so that small chunks of samples of different media types are alternated with one another.
572 Glossary
interleaved movie file A movie file in which the samples of different media types are interleaved to optimize playback performance. Compare to noninterleaved movie file.
intermovie communication internal media reference
See movie-to-movie communication. A reference to media data contained in the same
file.
keyed t e x t
A text track whose background color is keyed out.
key event pressed.
A wired event that is issued when keys on the keyboard are
key frame A full frame of video or sprite data. Compare to difference frame, override frame.
key frame rate
The m a x i m u m n u m b e r of frames that can occur before a
key frame is inserted in a sequence of compressed frames.
key state array On Windows, a 256-byte array that contains information about each of the 256 virtual-key codes. If a key is down, then the highorder bit (0x80) of the corresponding element of this array will be set.
layer
See sprite layer, track layer.
leaf atom
An atom (1) or atom (2) that contains no other atoms.
linked text Any part of a hypertext reference track that, when clicked or loaded, launches a specified URL in the user's Web browser. Compare to
clickable link, automatic link.
list t w e e n A tween that derives values from a list of atoms in an atom container, which can result in a series of discrete steps of noncontinuous values.
little endian A way of storing multibyte data in memory wherein the most significant byte is highest in memory. Compare to big endian.
looping state The setting of a movie that determines its playback behavior when it reaches the beginning or end of the movie. See also no looping, normal looping, and palindrome looping.
Iossless Said of a compressor if the result of compressing some data and then decompressing it yields the original data unchanged. Compare to Iossy. Iossy Said of a compressor if compressing and then decompressing data results in a change of the original data. Compare to Iossless. Macromedia Flash
See Flash.
media A structure in a QuickTime movie that contains information on the type and location of the media data for a track in the movie.
Glossary 573
media data metadata.
The data referenced by a media in a movie file. Compare to
media data reference A data reference to the file (or other media container) that holds the media data for a track in a movie. media editing session The period after calling BeginMediaEdits and before calling EndMediaEdi ts during w h i c h a media can be changed. media file A file that contains some or all of a movie's media data. Compare to movie file.
media handler A QuickTime component {of type MediaHandlerType) that knows how to interpret a specific kind of media data and present it to the user in the appropriate manner. media property atom An atom container that holds a child atom for one or more media properties (such as the background color of a sprite track). Compare to movie property atom. media sample A single element of media data. (For instance, a video frame is usually a single sample.) media structure
See media.
media time coordinate system to measure time in a media.
media time scale media time unit
A coordinate system that provides a means
The n u m b e r of media time units that elapse every second. The basic unit of time m e a s u r e m e n t for a media.
metadata Information in a QuickTime movie file that describes how the media data (for instance, the audio and video datal is organized and synchronized. Compare to media data. MIME
See Multipurpose Internet Mail Extension.
MIME type A text string used in Multipurpose Internet Mail Extension (MIME) transmissions to indicate the type of the data being transmitted.
modal-dialog event filter function
A function that is called to handle events
that occur in a modal dialog box.
modeless dialog box callback procedure A procedure on Windows that is called to handle events that occur in a modeless dialog box. modifier track A track that modifies the appearance or behavior of some other track. For instance, a tween track is a kind of modifier track that can be used to change the volume of a sound track as the movie progresses.
574 Glossary
movie A set of data (of type Movie) that is managed by the Movie Toolbox. A movie contains one or more tracks, each of which represents data of a specific type (for instance, video, sound, text, animation, and the like). movie annotation A piece of movie user data that provides descriptive information about the movie. movie atom An atom (of type 'moov') that contains movie metadata. Compare to movie resource.
movie callout function A function that the Movie Toolbox calls repeatedly while a movie preview is playing. movie controller
See movie controller component.
movie controller action Any one of a large number of actions that can be performed by a movie controller. movie controller action filter function An application-defined function that receives notification of pending movie controller actions; the filter function can handle those actions or pass them on to the movie controller. movie controller bar A visible set of controls for manipulating a movie, usually attached to the bottom of a movie.
movie controller component A QuickTime component that controls movie playback and editing; it may also manage a movie controller bar. movie controller control flags A 32-bit value whose bits encode the settings of various movie display and playback options. movie controller explicit flags A 32-bit value whose bits indicate which bits in the movie controller control flags are to be used explicitly (that is, to override any default behaviors of the movie controller). movie coordinate system A coordinate system in which the point (0, O) is at the upper-left corner of the movie rectangle. movie data atom An atom (of type 'mdat') whose data consists of one or more media samples, which together comprise the movie's media data.
movie data exchange component movie display clipping region movie image is to be drawn.
See movie exporter, movie importer.
The portion of the movie box in which the
movie export component A QuickTime component that writes QuickTime movie data in some other format. movie exporter movie file
See movie export component.
A file that contains a movie atom. Compare to media file.
Glossary 575
movie import component A QuickTime component that can convert certain kinds of files into movies.
movie importer See movie import component. movie information dialog box The dialog box displayed by a call to the ShowMovielnformation function, which contains descriptive information about a movie and a thumbnail image of the movie's poster image. movie-in-movie capability The ability to embed an entire QuickTime movie into some other QuickTime movie. Compare to child movie, parent movie. movie-loaded event An event that is issued whenever a child movie is loaded into a parent movie. movie matrix A matrix that maps the movie coordinate system into the display coordinate system. movie media handler
movie poster
The media handler that manages movie tracks.
See movie poster image.
movie poster image A single image that represents a QuickTime movie. Compare to movie preview.
movie poster time found.
The time in a movie at which the movie poster image is
movie poster track poster image.
A track in a movie that is used to create the movie
movie preprerolling completion routine An application-defined function that is executed immediately after a movie has been preprerolled. movie preview A short, dynamic representation of a QuickTime movie. Compare to file preview, movie poster image.
movie progress dialog box A dialog box that is displayed and updated by a movie progress function.
movie progress function A function that the Movie Toolbox calls when an operation (such as importing or exporting a movie) promises to take a significant amount of time. Compare to custom movie progress function, default
movie progress function.
movie property atom An atom container that contains information about a movie's properties. Compare to media property atom. movie resource A resource (of type 'moov') that contains movie metadata. Compare to movie atom.
516
Glossary
movie time coordinate system to measure time in a movie. movie time scale movie time unit
A coordinate system that provides a means
The number of movie time units that elapse every second. The basic unit of time measurement for a movie.
movie-to-movie communication The process of sending messages and actions from one movie to another. Movie Toolbox The part of QuickTime that provides services for opening movie files and extracting movies from them, playing movies, editing movies, creating movies, and much more. movie track
A track in a QuickTime movie that contains movie media data.
movie user data Some custom data that can be attached to a movie. This data is structured as a list of user data items. multimatrix tween A tween that derives values from concatenating two or more matrices produced by other kinds of tweens.
Multipurpose Internet Mail Extension (MIME) mitting binary data across the Internet.
A standard protocol for trans-
native endian The way of storing multibyte data in memory that is native to the host processor. no-interface movie controller A movie controller that operates just like the standard movie controller except that no controller bar is displayed and no keyboard events are passed to it. no looping The looping state of a movie that plays forward from beginning to end and then stops. Compare to normal looping, palindrome looping. none movie controller
See no-interface movie controller.
noninterleaved movie file A movie file in which samples of different media types are not interleaved. Compare to interleaved movie file. nonslavecl time base A time base that is not driven by some other time base. Compare to slaved time base. normal looping The looping state of a movie that plays forward from beginning to end and then returns to the beginning and plays forward again, and so on. Compare to no looping, palindrome looping. offset
See atom offset, track offset.
operand target The element of a QuickTime movie that an operand is directed toward.
Glossary 577
operator atom An atom that combines operand atoms (or perhaps other operator atoms) using numerical and logical operations.
override frame A difference frame contained in a sprite track. override sample interpretation mode A sprite track property that indicates how the sprite media handler interprets override samples.
palindrome looping The looping state of a movie that plays forward from beginning to end, then plays backwards from end to beginning, and then plays forward from beginning to end, and so on. Compare to no looping, normal looping. See also some demos. parent atom An atom (2) that contains one or more child atoms. parent movie A QuickTime movie that contains a child movie, using the movie-in-movie capability introduced in QuickTime 4.1. Pascal string A string that consists of a length byte followed by that number of characters. Compare to C string. path tween A tween that derives values based on the shape of an arbitrary curve defined by a vector path. playback hints A 32-bit value whose bits specify one or more settings or optimizations that the Movie Toolbox should use during movie playback. pointer data handler A data handler that can read and write data stored in memory that is accessed using a pointer and a length. pointer data reference A data reference that specifies a block of memory data accessed using a pointer and a length. A pointer data reference is a handle to a pointer data reference record.
pointer data reference record A structure (of type PointerDataRefRecord) that is used to create a pointer data reference.
poster See movie poster image. preprerolling The process of preparing a remote movie for prerolling. This may include setting up connections to remote servers and negotiating one or more protocols for exchanging data, as well as getting information about the types of data in the remote movie. Compare to prerolling. prerolling The process of preparing a movie for playback. This may include allocating buffers to hold data from the movie file and setting up one or more QuickTime components to decompress the data in the file. Prerolling is recommended for optimal playback performance.
preview See file preview, movie preview.
578 Glossary
preview atom record.
An atom (of type 'pnot') whose data is a preview resource
preview data atom An atom that contains the file preview data or that indicates where to find that data. preview data resource A resource that contains the file preview data or that indicates where to find that data.
A resource (of type 'pnot') whose data is a preview
preview resource resource record.
preview resource record A structure of type PreviewRes0urceRec0rd that indicates where to find a file preview. progress dialog box box. progress function
See image progress dialog box, movie progress dialog
See image progress function, movie progress function.
protocol
See scheme.
QT atom
See atom (2).
QTML
See QuickTime Media Layer.
QTVR See QuickTime VR. QuickTime A software architecture developed by Apple Computer, Inc. for creating and playing back multimedia content. QuickTime Media Layer (QTML) A library for Windows operating systems that supports the QuickTime application programming interfaces; the QuickTime Media Layer also provides an implementation of a number of the parts of the Macintosh Operating System (including the Memory Manager and the File Manager) and the Macintosh User Interface Toolbox {including the Dialog Manager, the Control Manager, and the Menu Manager}.
QuickTime movie file QuickTime movie
See movie file.
See movie.
QuickTime movie file format The format in which all of a movie file's media data and the information describing that data is stored. QuickTime VR The part of QuickTime that allows users to interactively explore and examine photorealistic, three-dimensional virtual worlds and objects. taw data
Data that is not compressed.
reference See data reference, media data reference.
Glossary 579
reference movie file A movie file that contains references to media data in some other file. Compare to self-contained movie file.
referring data The block of m e m o r y to which a data reference is a handle. Compare to target data. region code A value that specifies a version of a written language of a particular region of the world.
registration point
A property of a sprite image that determines the point of
rotation.
resource data handler
A data handler that can read data stored in a file's
resource fork.
resource data reference A data reference that specifies a resource in a resource file. A resource data reference is a handle to an alias record to which two pieces of information have been appended, a resource type and a resource ID. resource flipper A function that converts a handle of resource data by swapping the multibyte data fields in that handle, in place. root movie
The movie at the top of the parent/child hierarchy.
sample description
A data structure that contains information about a
sample.
scan code
See virtual key code.
scheme The initial portion of a URL, which precedes the first colon (:) and which specifies the protocol to be used to access the URL's data.
script editing
A mode of interacting with a text track w h e r e editing actions sent to the track are interpreted by the text media handler and executed. Compare to direct editing.
search flags A 16-bit value that can be passed to TextHediaFindNextText to control the text search. self-contained movie file A movie file that contains a movie's metadata and media data and therefore does not depend on any other files. Compare to reference movie file.
shortcut movie file A O uickTime movie file that refers to some other O_uickTime movie file.
single-fork movie file
A O uickTime movie file in which the movie's metadata and the movie's media data are stored in one file (which is the data fork on Macintosh systems). Compare to double-fork movie file.
580
Glossary
slaved time base A time base that is driven by some other time base. Compare to nonslavefl time base. some demos An important element of any QuickTime programming talk. See also palindrome looping.
spatial compression A means of compressing a single image by reducing redundant data in the image. Compare to temporal compression. spin tween
A tween that generates a series of matrices that spin a sprite around its registration point.
sprite A graphical object that has a number of properties, including its current image index, location, size, layer, graphics mode, and visibility state. sprite button behaviors A set of behaviors associated with a sprite that provide buttonlike capabilities to the sprite. sprite graphics mode
A property of a sprite that determines how the sprite is drawn into the sprite track.
sprite ID The atom ID of an atom that contains a sprite. sprite image index A property of a sprite that indicates which image from the set of sprite images in the current key frame is the sprite's current image.
sprite images container atom An atom (2) that contains the image data for various sprites.
sprite layer A property of a sprite that determines which sprite is drawn on top of the other sprite(s) when two or more sprites have locations that overlap.
sprite matrix A 3-by-3 matrix that controls the location, size, and rotation of the sprite image within the sprite track.
sprite property Any of several properties that can affect the appearance or behavior of a sprite.
sprite sample description A data structure that contains information about a sprite track.
sprite track property
Any of several properties that can affect the appearance or behavior of a sprite track.
sprite track variable Any of a set of variables associated with a sprite track whose values can be set and read by wired actions.
sprite visibility state A property of a sprite that controls whether it is currently visible.
Glossary 581
standard compression component See standard image compression dialog component. standard image compression dialog box A dialog box that contains controls for specifying image compression parameters. This dialog box is displayed by the standard image compression dialog component.
standard image compression dialog component
A QuickTime component that displays the standard image compression dialog box and provides other image compression services.
tag
Part of the data in a user data item of type 'p] ug'.
target
See action target, operand target, target data, target track.
target data The data picked out by a data reference. Compare to referring data.
target track (1) The track that is associated with a text track to make the text track a chapter track. The target track contains a track reference to the text track. {2) The track of a QuickTime movie that contains the action target or operand target.
tasking The process of explicitly allocating some processor time to QuickTime (often by calling the HoviesTask function) so that it can do whatever is required to keep a movie playing or downloading.
temporal compression A means of compressing a sequence of images by comparing two adjacent frames and storing only the differences between the two frames. Compare to spatial compression. test image The thumbnail picture displayed in the standard image compression dialog box. text
The written representation of a language.
text atom extension sample.
An atom of data that can follow the text data in a text
text callback procedure An application-defined function that is executed whenever the text media handler is about to display a new text sample. text description track.
A data structure that contains information about a text
text descriptor Any one of a large number of tags that can be inserted into imported text to control the format, appearance, or behavior of the text.
text movie import component A movie import component (of type MovieImportType and subtype TextMedi aType) that can import text into a movie.
582
Glossary
text movie settings dialog box A dialog box that allows the user to configure various settings of pasted or imported text. text options structure A data structure (of type TCTextOptions) that contains information about the appearance of a timecode. text track thumbnail
A track (of type TextMediaType) that contains text. A small copy of an image, typically 80 pixels on the longer side.
time base A set of values that defines the time coordinate system of the movie and defines the current movie time. time base control flags A 32-bit value whose bits encode the settings of various time base options, such as the movie's looping state. timecode A specific kind of time stamp that can be put on recorded media such as film, video, sound, and the like. timecode counter record A data structure (of type TimeCodeCounter) that specifies a timecode in terms of a counter value. Compare to timecode time record. timecode definition structure A data structure (of type TimeCodeDef) that contains information about the format of a timecode. timecode media handler A media handler that is used to create timecode tracks in movies, hide and show those tracks, get and set information about timecode tracks, and so forth. timecode record A data structure (of type TimeCodeRecord) that specifies a timecode. See also timecode counter record, timecode time record. timecode sample description A data structure (of type TimeCodeDescript i on) that contains information about a timecode track.
timecode time record A data structure (of type TimeCodeTime) that specifies a timecode in terms of hours, minutes, seconds, and frames. Compare to timecode counter record. timecode track data. timecoding time scale time unit
A track (of type TimeCodeMediaType) that contains timecode
A method of applying timecodes to recorded media. See media time scale, movie time scale. See media time unit, movie time unit.
track A structure in a QuickTime movie that represents a single stream of data. A track has a starting time and duration and is associated with exactly one kind of media data. Compare to media.
Glossary 583
track coordinate system A coordinate system in which the point (0, 0) is at the upper-left corner of the track rectangle. track layer A track property that determines the order in which the track is drawn into the movie. track matrix A matrix that maps the track coordinate system into the movie coordinate system. track offset The empty space between the beginning of the movie and the beginning of the track data. track rectangle
The rectangle that encloses the visible portion os a track.
track reference A reference or link from one track to another that establishes a relationship between those two tracks. For example, a track reference of type kTrackReferenceChapterLi st is used to create a chapter track. track usage A long integer whose bits indicate w h e t h e r the track is used in the movie, the movie poster, the movie preview, or any combination of these. track user data Some custom data that can be attached to a track. This data is structured as a list of user data items. transcoding The process of converting data stored in one compression format into another. tween data atom tween.
An atom (of type kTweenData) that contains the data of a
tween duration atom duration of a t w e e n . tween entry
An atom (of type kTweenDuration) that indicates the
An atom (of type kTweenEntry) that holds other tween atoms.
tweening The process of generating values that lie between two given values or that are in some other way algorithmically derived from some given data.
An atom (of type kTweenStartOffset) that indicates how far into the tween media sample the tweening operation is to begin.
tween offset atom tween track
A track that contains tween data.
tween type atom tween.
An atom (of type kTweenType) that indicates the type of a
URL data handler A data handler that can read data from locations specified using uniform resource locators (URLs).
584
Glossary
URL data reference A data reference that specifies the location of some data using a URL. A URL data reference is a handle to a N U L L - t e r m i n a t e d string of characters that comprise the URL. user data item A single element of a user data list. See also movie user data, track user data. user data list track.
A list of user data items that can be attached to a movie or a
user item callback procedure A p r o c e d u r e called by the Dialog M a n a g e r to handle user items in a dialog. validating The process of opening an instance of a graphics i m p o r t e r and instructing it to examine some of the data in an image file to see w h e t h e r it can handle that kind of data. variable ID A value (of type QTAtomID) that identifies a sprite track variable. virtual key code A value that represents a specific physical key on a specific model of keyboard. wired action An action (such as setting a movie's time) that is initiated by some particular event. The wired actions include both user events like moving and movie controller events like loading movies or from the operating system.
volume or events that or clicking processing
its c u r r e n t can trigger the m o u s e idle events
wired sprite A sprite that can initiate w i r e d actions. A wired sprite is a sprite to w h i c h one or more event atoms have been attached. wired text
A part of a text track that can initiate a wired action.
Glossary 585
586
Glossary
Index
A action atoms, adding, 484-485 action flags, 510 action list, 489 action parameter atom, 485 actions adding, to event atoms, 484 case atoms for, 489, 490 conditional, 494-495 default targets, 504 kActionCase, 488, 489, 492, 557 kActionGoToURL, 470, 536, 548 kActionMovieGoToBeginning, 469, 473, 475 kActi onMovieGoToEnd, 469 kAct i onMovieGoToTime, 469 kAct i'onMovieGoToTimeByName, 469 kActionMovieSetLoopi ngFl ags, 469 kActi onMovieSetRate, 469 kActionMovieSetSelection, 469, 470 kActi onMovieSetVol ume, 469 kActi onMovieStepBackward, 469 kAct i onMovieStepForward, 469 kActi onSpri teSetGraphi csMode, 469 kActionSpriteSetlmagelndex, 469, 500, 527 kActionSpri teSetLayer, 469 kActi onSpri teSetMatri x, 469 kActi onSpri teSetVi si bl e, 469 kActi onSpri teTrackConcatVari abl es, 488 kActionSpriteTrackSetVariable, 292, 487
kAct i onSpri teTrac kSetVari abl etoStri ng, 487 kActionSpriteTranslate, 494, 495, 498, 504, 556 kActionYextTrackPasteText, 544, 545, 550 kActi onTextTrackSetEdi tabl e, 544 kAct i onTrackSetBal ance, 469 kActionTrackSetCl ip, 469 kActionTrackSetCursor, 469, 498 kActionTrackSetEnabled, 469, 510 kActi onTrackSetGraphi csMode, 469 kActi onTrackSetLayer, 469 kActi onTrackSetMatri x, 469 kActionTrackSetVol ume, 469 kActionWhi le, 488 mcActionControllerSizeChanged, 16, 17, 69 mcActionGetExternaIMovie, 518, 519 mcActi onGetMovieID, 524-525 mcActi onGetMovieName, 524-526 mcActi onMouseDown,431 mcActi onSetLoopi ng, 44-45 mcActionSetLooplsPal i ndrome, 44 mcActionSetSelectionBegin, 36, 37 mcActionSetSelectionDuration, 36, 37 movie controller, 16-17, 518-519 samples of, 468-470 specifying, 468-470 on sprite tracks, 469 targets of, 466 text. See text actions
587
actions/continued) on tracks, 469 types of, 468-470 while atoms for, 488-489 wired. See draggable sprites; wired actions; wired sprites without targets, 470 AddEmptyTrackToMovie function, 254 AddFilePreview function, 199 adding in parallel, 292, 315, 316 AddMediaSample function, 166-167, 169-170, 240, 241 text, 294, 296 timecodes, 330 video, 176-177 Add menu item, 283, 284 AddMovieResource function, 156, 160, 163, 168, 250 AddMovieSelection function, 439 Add Scaled command, 284, 285, 291 AddTrackReference function, 301,311,313, 340, 453 AddUserDataText function, 206, 210, 211, 327 AIFF audio files, 91-92 alias data handler, 247. See also file data handler aliases, 247, 248-249 Alias Manager, 249 anchor points, 97 application framework basic services, 4-5 editing movies, 18-20 events, 17-18 expanding, 91-95 file structures, 5 frame window, 1-2 menus, 11-13 movie controllers, 1-4 movie windows, 6-11 still images, 92-93 updating, 59-61 Windows vs. Macintosh, 4-5 application-specific data, 56-59 attaching, to movie windows, 58-59
588
Index
attaching, to window objects, 58 setting up window objects, 57-58 asynchronous compression, 397-401 defined, 398 overview, 397-398 performing, 399-400 setting up for, 398-399 signaling end of, 400 weighing benefits of, 401 asynchronous file transfer, 265-268 buffers, 265 functions, 265-266 read operation responses, 267-268 write operation responses, 266-267 atom container atom defined, 216, 230 identifying, 230 atom containers, 229-234 atom data from, 233-234 complexity of, 230-231 creating, 231-233 defined, 124, 216, 230 examples, 230-231 exporting movies and, 124 features, 216 finding atoms in, 233 hierarchical arrangement, 215, 226, 229-231 Internet connection speed examples, 234-237 overview, 229-231 atom header defined, 155 filling in, 200, 201 atoms child, 230, 232-233 counting, 222-223 data, filling in, 200-201 data, getting, 233-234 defined, 155, 216 finding, 219-222, 233 flexibility of, 215 freeing, 218, 225-226 hierarchical arrangement, 215, 226, 229-231
last in file, 223 leaf, 226, 230 parent, 230, 232-233 preview. See file previews preview data, finding, 223-224 removing, 224-225 type/ID, 230 types of, 216 audio files, converting to movies, 91-92
U badges, 24-25 basic interactions, 21 BeginMediaEdits function, 161, 166, 250, 294, 330, 394, 441 Best Depth menu option, disabling, 389 B-frames, 376 big-endian format, 39 atom container data, 236 defined, 344 leaf atoms, 230 little-endian vs., 344-345 movie user data, 345-347 storage, 347 structure, 344, 345 BMP format, 85 border (picture-in-picture movies), 78-79, 80-81 bouncing sprites, 553-560 colliding together, 560-562 moving sprites, 553-556 overview, 552 track rectangle collisions, 556-560 buttons controller bar, 28-32 hiding, 28, 32 QuickTime VR controller, 29-32 showing, 28-29, 31 sprites acting like. See spriteIsl; wired sprites standard controller, 28-29 See also specilic buttons
button-trigger events, 475 byte swapping defined, 345 guidelines, 347 movie user data and, 345-347
C callback procedures, 142-143 extended procedures, 379-383 modeless dialog box, 362-363 user item, 358-359 Cal IUniversal Proc function, 371
Cancel button, 207 canceling current operation, 141 Carbon, 367-371 accessing data structure fields, 368-369 compatible APIs, 10, 113, 180-181 eliciting user file, 369-371 features, 367-368 origin, 344 as porting layer, 367-368 QTMakeMovie supporting, 180-181 string support, 364 unsupported functions, replacing, 369-371 UPPs, 108, 371 case atoms, 489, 490 cel animation, 405 chapter tracks, 310-313 chapters and, 279 creating, 311 defined, 277 determining presence of, 313 illustrated, 279, 280 linked text, 279 modifier tracks, 311 overview, 277-279 pop-up menu, 277-279 setting, 312 target tracks, 310 track references, 310-311 turning text track into, 311-312 unsetting, 312
Index 589
child atoms, 230, 232-233 child movies, 238 chunk atoms, 216 classic atoms, 216 clearing current movie, 67 Clear menu item, 283, 284 ClearMovieSelection function, 67 clicks custom button, 34-35 detecting, 430-431 hit-testing sprites, 430-434 looking for, 141 See also mouse button client software set-up, 55-56 codecs, 169, 175, 375-376, 401,426, 432 collisions namespace, 351-352 sprite, 556-562 track rectangle, 556-560 Component Manager finding graphics import components, 114 opening controllers, 48-49 Compress Image function, 174, 175-176, 373 compressing images, 377-386 dialog box. See standard image compression dialog box extended procedures, 379-383 filtering events for, 380-381 hook functions, 381-382 image pixel maps, 377-378 QTCmpr CompressImage function, 383-385 restricting compressor types, 386 test image, 378-379 compressing image sequences, 386-397 configuring standard image compression dialog component, 389-390 creating new movie file/movie, 392-394 into destination movie, 394-396 dialog box, 386-387, 391-392 extended procedures, 391 finishing up, 396-397 getting image sequence for, 387-389 overview, 386-387 preparation steps, 387-394
590
Index
target movie, 392-394 test image, 390-391 user changing frame rate, 392 compression, 373-401 asynchronous, 397-401 codecs for, 169, 175, 375-376, 401,426, 432 data buffer, 175-176 decompression and, 375-376 defined, 375 functions, 174 maximum size, 174-175 prerequisite, 174 spatial, 376-377 sprite, 420-421,422, 426 temporal, 376-377 types of, 376-377 video frame, 173-176 See also compressing images; compressing image sequences; standard image compression dialog component Compress menu item, 377 conditional actions, 494-495 conditional expression, 493-494 conditions, creating, 492-493 Connection Speed panel, 234, 235 constant operands, 491 container atoms building, example, 226-229 defined, 216, 226 preview atoms vs., 226 control flags QuickTime VR buttons, 29-32 setting appropriate bit, 31 standard controller buttons, 28-29 See also specific control flag types
controller bar. See movie controller bar controllerless movies, 51-52 converting data types, 366-367 ConvertMovieToFile function, 118, 119, 120, 121, 122, 123 coordinate systems display, 480 media time, 165, 166
movie, 480 movie time, 153, 165-166 tracks, 479-480 CopyTrackSettings function, 251,254 counters, 323, 326-327 CreateMovieFi]e function, 160, 163, 164, 166, 168, 250 CreatePortAssoci ation function, 8 CreateShortcutMovieFi]e function, 227, 229 CreateWi ndowEx function, 14 cross-fade, 171 cross-platform code, 343-372 avoiding namespace collisions, 351-352 converting data types, 366-367 endian issues, 344-350 files, 352-354 getting stored window position, 345-346 menus, 12-13 modal dialog boxes, 355-361 modeless dialog boxes, 361-363 movie user data, 345-347 overview/background, 343-344 reading/writing resource data, 347-350 resource files, 354-355 strings, 363-366 text, 367 See also QuickTime Media Layer (QTML) C strings converting Pascal strings to, 365-366 converting to Pascal strings, 363-365 as parameters, 364 specifications, 363 current text, 307-308 cursors
built-in, 498 key events and, 550 custom button, 32-36 clicks, 34-35 description, 32 displaying, 32 illustrated, 32 managing, 28, 29 custom progress functions, 134-147 callback procedure, 142-143
canceling current operation, 141 closing dialog box, 143 complete code, 143-147 dialog box example, 134-135 displaying estimated time remaining, 139-140 opening dialog box, 137-138 overview, 134-137 parameters, 135-136 progress messages, 138-140 searching key presses, 141-142 searching Stop button clicks, 141-142 user actions, 140-143 See also progress functions custom resource data flipping, 348-350 Cut menu, 18-19 cut movies, 51 CutMovieSe]ection function, 67 cut scenes, 51 cutting entire movie selection, 69
D data fork, 154-155, 163, 168 data handler completion function, 266 data handlers closing down, 269-270 component types, 247 configuring, 264 defined, 245 opening, 264 overview, 246-248 private, 247 standard, 245, 246-247 tasking, 268-269 See also speci/ic data handlers DataHGetData function, 265 DataHGetFi]eSizeAsync function, 272 DataHGetFi ]eSize function, 272 DataHPutData function, 265 DataHReadAsync function, 265-266, 267, 269 DataHTask function, 268 DataHWrite function, 265-266, 269
Index 591
data reference extensions, 246, 273-275 appending, 274-275 defined, 274 MIME type, 275 types of, 274 data reference record, 255-256 data references as atoms, 245 defined, 245, 247 referring data, 248 target data, 248 types of, 247-248 for uncreated files, 249 See also data handlers data structures, accessing, 368-369 decompression, 375-376 OeleteTrackReference function, 311,313 DeleteTrackSegment function, 306, 309 delta frames, 376 dialog boxes cross-platform code, 355-363 custom progress functions, 134-135, 137-138, 143 modal, 355-361 modeless, 361-363 user item callback procedure, 358-359 See also specific dialog box names Dial ogSel ect function, 361,362 difference frames, 376, 407 direct editing, 544 directional sound, 533 Di sableMenultem function, 13 display clipping region calculating, 77-78, 82 default, 77 defined, 77 picture-in-picture movies, 77-79 setting, 77-79 display coordinate system, 480 Di sposeMovi eEdi tState function, 68 Di sposeMovi eTrack function, 194, 340 Di sposeRouti neDescri ptor function, 371 double-fork movie files accessing file previews, 197
592
Index
converting to single-fork, 158 disadvantage of, 155 illustrated, 155 single-fork vs., 155-156 structure of, 154-156 OragAl i gnedWindow function, 94 draggable sprites, 491-498 button behaviors, 498-500 code summary, 495-498 conditional actions, 494-495 conditional expression, 493-494 creating conditions, 492-493 event atoms for, 492 illustrated, 464 overview, 464, 491-492 OrawOialog function, 138, 359 drawing images, 89, 90 image windows, 94-95 picture-in-picture movies, 80-81 rotated images, 101-102 OrawPi cture function, 390 DrawString function, 363, 364 drop-frame timecode, 319, 322 duration atom, 457
E Edit Annotation dialog box, 205, 206-208 configuring, 207 dialog item list, 206-207 setting label, 207-208 showing current annotation, 208-210 static text item, 207 editing, 18-20 changed window sizes and, 69 cutting, 18-19, 69 deleting edit state, 68 direct, 544 enabling, on text tracks, 544-545 with Movie Toolbox, 67-71 pasting, 19
reinstating previous state, 68 script, 544-545 text, 306-310, 544-545 tracking current state, 68 turning on, 18 Edit menu, 281-291 adjusting, 282-283, 289-291 alternative views, 283-285 emulating QuickTime Player, 283-285 enabling/disabling items, 20, 281, 283-285, 288 functions, 19 keyboard modifiers and, 283-284, 285-287 movie controller handling, 18-20 Movie Toolbox handling, 69-71 renaming items, on Windows, 287-289 edit states, 68 embedding movies, 52-53, 54. See also movie tracks; picture-in-picture movies EnableMenultem function, 13 endian issues, 344-350 big- vs. little-endian, 344-345 byte swapping, 345-347 getting stored window position, 345-346 movie user data and, 345-347 reading/writing resource data, 347-350 storage format, 347 EndMediaEdits function, 161, 167, 250, 294, 330, 441 Enter'Movies function, 56 Escape key presses, 142 event atoms adding, 483-484 adding action atom to, 484 defined, 466 structure of, 466, 467 events filtering, 380-381 handling, 17-18 intercepting, in hook function, 382 issued, 467 key, 536, 549-551 in modeless dialog boxes, 361-363
movie-loaded, 531-532 translating, 18 Windows vs. Macintosh, 4-5 wired sprite, 466-468 See also specific events Exi tMovi es function, 56 expanding application framework, 91-95 explicit flags, 29-32 exporting images, 106-112 directly, 109-112 options dialog box, 107, 108 to specified format, 111-112 to user-selected format, 109 using export image dialog box, 107-109 exporting movies, 118-126 to any available format, 120-121 available options, 119-120 defined, 118 dialog box, 119 flags, 119-120, 122 functions for, 118 as hinted movies, 125-126 hinter component example, 121-126 parameters, 118-120 settings, 123-126 to specific format, 121-123 expression container atoms, 490-491 extended procedures filtering dialog box events, 380-381 hook functions, 381-382 image compression, 379-383 image sequence compression, 391 installing, 382-383, 391 extended procedures structure, 379-380 external movie targets. See movie-to-movie communication external references, 158
F Fast Start movie files, 156-157, 160. See also QuickTime movie file creation file conversion dialog box, 129
Index 593
file data handler, 248-254 component subtype, 247 for file transfers. See file transfers opening movie files, 249 uses, 246 file data reference aliases and, 247, 248-249 creating, 248 creating reference movie files, 250-254 defined, 247 file filter function, 113 File Manager data format from, 347 managing non-movie files, 353-354 using functions from, 352 filenames appending reference extensions to, 274-275 appending to referring data, 273-274 file previews, 196-204, 216-226 accessing, 197-198 container atoms, 216, 226-229 counting atoms, 222-223 creating, 198-204 defaults, 197 defined, 197 filling atom data, 200-201 filling atom header, 200 finding atoms, 219-222 finding preview data atom, 223-224 freeing atoms, 225-226 overview, 196-197, 216-217 preview atoms vs. container atoms, 226 preview atoms vs. preview data atoms, 217 removing atoms, 224-225 removing existing, 217-219 thumbnail for, 201-202 writing atom data, 201 files cross-platform code, 352-354 reading data into handle, 353-354 file system specifications {FSSpec), 352
594
Index
file transfers, 262-273 asynchronous, 265-268 beginning, 266 closing data handlers, 269-270 copying remote to local file, 270-272 effectiveness of, 272-273 finishing up, 269-273 global variables, 262-263 local file for, 263 overview, 262-263 synchronous, 264-265 tasking data handlers, 268-269 timer callbacks, 269 Fill Screen menu command, 86 film timecodes, 321 filtering events, 380-381 filtering out movies, 127-128 FindNextComponent function, 121 flags. See speci/ic flag types Flash, 534 Flash tracks, 534 flattened movie files, 158-159, 250 F1attenMovi eData function creating movie in RAM, 255-257 Fast Start movie files and, 157, 179 interleaved movie files and, 159, 160 reference movie files and, 158 single-fork/double-fork files and, 156 flipping data, 348-350 flipping images, 97-100 horizontally, 97-98, 99-100 vertically, 99-100 See also image matrices floating-point variables, 487-488 frame-loaded events, 467-468, 484, 504 frame rate adjusting, 392 tween tracks, 455 frame rate field, 389-390 frames counting, 388-389 getting next, of source movie, 395 key, 376 types of, 376
frames per second (fps), 321-323 frame window, 1-2 free atoms, 218, 225-226 FSSpec. See file system specifications function operands, 491,495 functions asynchronous file transfer, 265-266 compression, 174 custom progress. See custom progress functions graphics exporters, 106 hook, 381-383 image matrices, 97 image progress, 147-148 movie controller action filter, 16-17 renamed Mac versions, 351 See also specific functions
G geometry picture-in-picture movies, 76-77 timecode tracks, 330-332 See also matrices; rectangles Gestalt function, 55 GetDataflandler function, 258, 264 GetOesktopWi ndow function, 15 GetOialogltem function, 208, 210, 359 GetGraphicslmporterForOataRef function, 273 GetGraphicslmporterForFile function, 88, 93, 273 GetHandl eSi ze function, 231 GetKeyboardState function, 286-287 GetMaxCompressionSize function, 174-175, 176, 373 GetMedi aHandl erOescri pti on function, 251 GetMedi aHandl er function, 297, 323-324 GetMedialnputMap function, 444-445 GetMenuID function, 368-369 GetMovieBox function, 14, 59-60, 480 GetMovielndTrack function, 190 GetMovielndTrackType function, 190, 251, 323-324, 387
GetMoviePlayHints function, 73 GetMovi ePosterPi ct function, 390 GetMovi ePreferredRate function, 63 GetMovi ePropertyAtom function, 532 GetMovieSelection function, 191, 192 GetMovi eTimeBase function, 72 GetMovieTime function, 62-63, 187, 308 GetMovieTrackCount function, 190 GetMovieUserData function, 38-39, 205-206, 208, 345 GetNati veWindowPort function, 8 GetNextWindowfunction, 10 GetQuickTimePreference function, 234, 235 GetScri ptManagerVariabl e function, 209, 211 GetTimeBaseFl ags function, 72 GetTrackDimensions function, 251,297, 330 GetTrackMedia function, 251 GetTrackNextInterestingTime function, 308, 309, 388 GetTrackUsage function, 189 GetUserDataltem function, 205-206, 345 GetUserDataText function, 39, 206, 208, 209 GetWindowRect function, 15 GoToBeginni ngOfMovie function, 65 Go To Poster Frame menu item, 188, 190 Go-To-Start sprite button-trigger events, 475 event atoms for, 473 mouse click events, 473-474 mouse-up events, 474-475 setting properties, 471-473 structure of, 475, 476 graphics. See images GraphicsExportDoExport function, 111, 112 graphics exporters defined, 106 exporting images directly, 109-112 features, 85, 106 functions, 106 using export image dialog box, 107-109 GraphicsExportSetInputGraphicslmporter function, 111 Graphi csExportSet0utputFi ]e function, 111
Index 595
Graphi csExportSetOutputFi I eTypeAndCreator function, 111 Graphi cs ImportDoExport ImageFi I eDi al og
function, 106 exporting using dialog box, 107-109 parameters, 108-109 graphics importers closing, 95 data reference extensions, 273-275 defined, 87 determining image presence, 88-89 determining image types, 88-89, 273 features, 85, 87 for image pixel map, 377-378 Graphics ImportExportlmageFi I e function, 106, 110 Graphics ImportGetBoundsRect function, 99, 101 Graphics ImportGetMatri x function, 98-99 GraphicsImportSaveAsPicture function, 106 Graphi cs ImportSaveAsQui ckTimeImageFi I e
function, 106 Graphics ImportSetMatri x function, 99 graphics mode tween, 449-455 adding sample to, 452-453 adding tween track, 450-453 input map, 453-455 media sample, building, 451-452 opaque mode, 452 penguin movie structure, 449-450 track reference, 453 transparent mode, 452 graphics port setting, of graphics importer, 89 window association, 8 graphics windows, 94-95 movie windows vs., 94 redrawing, 94-95 sizing, 94 gReadDataCompletionUPP function, 263, 267 gWriteDataHComp]etionUPP function, 263, 268
596
Index
H Half Size menu command, 102-103 handle data handler, 254-257 component subtype, 247 creating movie in tLAM, 255-257 uses, 246, 254, 255 handle data reference creating, 254-255 data references for, 274-275 defined, 247 MIME types for, 275 height [controller bar}, 26, 27 HFS data handler, 247. See also file data handler highlighting text, 303 hinter export component, 121-126 hit-testing sprites, 430-434 hook functions, 381-383 hotspots, 498, 533, 534 HREF track, 314-315 defined, 279 determining presence of, 315 disabled, 298 naming, 314 searching for, 279, 298 setting, 314 unsetting, 314 hypertext actions, 547-549. See also text actions hypertext atoms, 535, 536, 537, 547-549 hypertext reference (HREF) track. See HREF track
ICM (Image Compression Manager), 174, 198, 373, 398 ICM completion procedure record, 398-399 ICUti ] s_RecompressPi ctureWi thTransparency function, 421 identity matrix, 97
idle-time tasks, 66, 79 IETF {Internet Engineering Task Force), 248 I-frames, 376 if-then emulation, 489 Image Compression Manager (ICM), 174, 198, 373, 398 image description, 170 image index, external sprite, 527-529 image matrices anchor points, 97 defined, 96 examples, 97-98 Fixed type, 97
flipping images, 97-100 functions, 97 identity matrix, 97 operation order, 98 rotating images, 100-102 scaling images, 97-98, 102-105 transformation options, 97 Image menu adding checkmark to, 86 handling items in, 95-96 QTGraphics vs. PictureViewer, 86-87 image progress functions defining, 147-148 displaying custom dialog box, 148 images compressing. See compressing images; compression; standard image compression dialog component converting into movies, 91-92 determining presence of, 88-89 determining types of, 88-89 displaying, 87, 105-106 drawing, 89, 90 exporting, 106-112 file formats, 85 finding files of, 112-114 flipping horizontally, 97-98 in graphics files, 105-106 importing, 87-91 matrices of. See image matrices media types, 85
multi-image files, 105-106 opening, 87, 90-92 overview, 85-87 pixel maps, 377-378 rotating, 100-102 scaling, 97-98, 102-105 still, handling, 92-95 validating, 88-89 windows of, 94-95 See also graphics exporters; graphics importers image sequences compressing. See compressing image sequences getting, 387-389 tracking movie time, 388 importing files, 127-133 images, 87-91 text, 291-294 importing files, 127-133 combined code, 131-132 conversion dialog box, 129 filtering out movies and, 127-128 overview, 127 in place, 129-130 I n i t i a l izeOTML function, 55, 56
input map, 442-447 adding video override to, 443-444 child atoms, 442-443 creating, 444 defined, 442 graphics mode tween, 453-455 video override structure, 443 InsertMedialntoTrack function, 160, 167-168, 294, 306, 310, 330 InsertTrackSegment function, 251,439 Inside Macintosh series, 358, 359 interframes, 376 interleaved movie files, 159. See also QuickTime movie file creation intermovie communication. See movie-tomovie communication internal references, 158
Index 597
Internet connection speed, 234-237 Connection Speed panel, 234, 235 identifying supported speeds, 235 retrieving user preferences, 234-236 setting user preference, 237 Internet Engineering Task Force (IETF}, 248 intraframes, 376 InverseMatri x function, 480 I sDi al ogEvent function, 361,362 issued events, 467
$-K JPEG files, 85, 373 kActionCase action, 488, 489, 492, 557 kActionGoToURL action, 470, 536, 548 kActionMovieGoToBeginning action, 469, 473, 475 kActi onMovieGoToEnd action, 469 kActionMovieGoToTime action, 469 kAct i onMovieGoToTimeByNameaction, 469 kActi onMovieSetLoopi ngF1ags action, 469 kActi onMovieSetRate action, 469 kActionMovieSetSelection action, 469, 470 kActionMovieSetVolume action, 469 kAct i onMovi eSt epBackward action, 469 kActi onMovieStepForward action, 469 kActi onSpri teSetGraphi csMode action, 469 kActionSpriteSetImageIndex action, 469, 500, 527 kActi onSpri teSetLayer action, 469 kActionSpriteSetMatrix action, 469 kActionSpriteSetVisible action, 469 kActi onSpri teTrackConcatVari abl es action, 488 kActionSpriteTrackSetVariable action, 292, 487 kActi onSpri teTrackSetVari abl etoStri ng
action, 487 kActionSpriteTranslate action, 494, 495, 498, 504, 556 kActionTextTrackPasteText action, 544, 545, 55O
598
Index
kActi onTextTrackSetEdi tabl e action, 544 kActionTrackSetBalance action, 469 kActionTrackSetCl ip action, 469 kActionTrackSetCursor action, 469, 498 kActionTrackSetEnabled action, 469, 510 kActi onTrackSetGraphi csMode action, 469 kActionTrackSetLayer action, 469 kActionTrackSetMatrix action, 469 kActionTrackSetVol ume action, 469 kAct i onWhi I e action, 488
keyboard modifiers, 283-284, 285-287, 550 keyDown events, 141 keyed text defined, 277 illustrated, 278 key events, 549-551 key frames, 376. See also sprite key frame samples key presses, finding, 141-142 key state array, 286-287 kOTEventFrameLoaded event, 466, 467-468, 484 kQTEventldle event, 466, 468 kQTEventKey event, 468 kQTEventMouseC1i ckEnd event, 466, 467 kQTEventMouseC1i ckEndTri ggerButton event, 466, 467, 475 kQTEventMouseClick event, 466, 467 kQTEventMouseEnter event, 466, 467 kQTEventMouseExit event, 466, 467
I. layers sprite, 406, 414 track, 477-478 leaf atoms, 226, 230 linear movies, 21 linked text, 279 list tween, 449 little-endian byte ordering big-endian vs., 344-345 defined, 344 movie user data, 345-347
storage, 347 structure of, 344, 345 local file copying, to remote file, 270-272 creating, 263 Local ToG1obal function, 34 looping movies with Movie Toolbox, 71-75 with movie tracks, 237-243 looping states determining, 71-72, 74-75 getting, 43-44 manipulating, 43-46, 72-74 modeless dialog boxes, 361-363 no looping, 43, 72 playback hints, 73 possibilities, 43 reading, 72 setting, 44-46, 72-74 specifying, 43 storing, 45-46 time base and, 72 types of, 43 See also normal looping; palindrome looping
IW MacOffsetRect function, 81
Macromedia Flash, 534 Mac-style keyboard modifiers, 286-287 MakeFi ]ePreview function, 198-199, 200 Make Memory Display Movie menu item, 540 MakeThumbnai ] FromPicture function, 201-202 matrices image, 97-105 movie, 480 sprite, 406 track, 480-481 matrix tweening, 455-457 building data atom, 455-456 duration atom, 457
offset atom, 456-457 spin tweening and, 457 See also multimatrix tweens; spin tweening mcActionControllerSizeChanged action, 16, 17, 69 mcActionGetExternalMovie action, 518, 519 mcActi onGetMovieID action, 524-525 mcActi onGetMovieName action, 524-526 mcActi onMouseDown action, 431 mcActi onSetLoopi ng action, 44-45 mcActionSetLoopIsPa] indrome action, 44 mcActionSetSelectionBegin action, 36, 37 mcActionSetSe]ectionDuration action, 36, 37 MCCopyfunction, 60 MCCut function, 18-19, 60, 67 MCOoActi on function, 15-16, 72 setting current movie time, 303 setting movie play rate, 187, 189 MCEnableEditing function, 18 MCGetControl I erBoundsRect function, 26, 59-60 MCGetControllerlnfo function, 283, 284, 289 MCGetMenuString function, 288 MCGetVisible function, 23 MCIsPlayerEvent function, 17, 18, 291, 315, 317, 434 MCMovieChanged function, 187-188, 189, 191, 313, 340 MCPaste function, 288, 291,292 MCSetContro] ]erAttached function, 25 MCSetUpEditMenu function, 284-285, 287-288, 289 MCSetVisible function, 22, 23, 24 MCUndofunction, 67 MDI (multiple document interfacel, 5 media (structurel adding new, 164-166 adding samples to. See media samples (adding} associating track with, 164-165 common types, 165 defined, 153 example, 153-154
Index S99
media (structure){continued] segments, inserting, 167-168 specifying type, 164-165 time scale, 165 media data atom container, 155 metadata separated from, 154 QuickTime movie structure and, 152 reference movie files and. See reference movie files media data reference, 166 media editing sessions, 166 media files defined, 250 movie files and, 250 in reference movie files, 157-158, 250 media handler, 165, 245 media property atoms, 424, 475 media sample, defined, 155 media samples (adding), 169-179 compressing video frames, 173-176 data types, 169 drawing video frames, 170-173 image description, 170 media editing sessions for, 166 overview, 166-167, 169-170 requirements, 169 sample description, 169-170 text, 294-297, 306 timecode, 328-330 video frames, 176-179 media time coordinate system, 165, 166 media time units, 165 media types, 85 Memory Manager, 255, 261 menu IDs, 1 1 - 1 2 menus adding, 5 adjusting, 5 cross-platform references, 12-13 enabling/disabling items, 13, 20 handling, 11-13 identifying, 11-12 QTApp_AdjustMenus function, 5, 104-105
600
index
metadata defined, 152 double-fork files, 154-155 media data separated from, 154 single-fork files, 155, 156 storage, 155-156 tracking, 156 MIME (Multipurpose Internet Mail Extensionl, 275 MIME type data reference extensions, 275 modal dialog boxes cross-platform code, 355-361 drawing user items, 356, 357-358 pop-up menu control, 359, 360-361 modal-dialog event filter function, 380 ModalDialog function, 140, 141,210 modeless dialog boxes, 361-363 callback procedure, 362-363 Macintosh, 361-362 Windows, 362-363 modifier tracks, 311,435, 442, 443 mouse button clicked, event atoms, 473-474, 492 conditional action of, 492-495 draggable sprites and. See draggable sprites hit-testing sprites, 430-434 released, event atom, 492 triggering wired actions, 280, 466-468 See also clicks mouse-up events, 474-475 movie annotations, 204-214 constants, 205 defined, 204 edit dialog box, 205, 206-208 editing code, 211-213 example, 204-205 overview, 104-106 retrieving, 210-214 showing current, 208-210 movie atom adding, 168 defined, 155 location, 156 movie resource vs., 156
movie callout function, 193 movie controller action filter functions, 16-17, 518-519 movie controller actions mcActionControllerSizeChanged, 16, 17, 69 mcActionGetExternaIMovie, 518, 519 mcActi onGetMovieID, 524-525 mcActi onGetMovieName, 524-526 mcActi onMouseDown, 431 mcActi onSetLooping, 44-45 mcActi onSetLoopI sPaI i ndrome, 44 mcActionSetSelectionBegin, 36, 37 mcActionSetSelectionDuration, 36, 37
movie controller bar, 2 attaching, 25 badges and, 24-25 buttons, 28-32 custom button, 28, 29, 32-36 detaching, 25, 26 determining visibility, 23 height, 26, 27 hiding, 22, 23 illustrated, 3, 4 managing, 2-4, 22-27 placing on top, 25-26, 27 QuickTime VR, 4 showing, 22, 23 specifying location, 25-26 sprites and, 464-465 standard buttons, 28-29 toggling visibility, 24 movie controller component, 2 movie controllers, 1-4 actions of, 518- 519 attaching, 14-17 badges and, 24-25 basic interactions, 21 changing type, 41-42 defined, 2 features summary, 4 filters, 16-17 finding movie targets, 518-522 impending action notification, 15-16 intercepting actions, 16-17
need for, 50 no-interface, 478-479 opening URLs and, 48-50 rectangle definition, 14-15 setting, 40-42 specifying type, 39-42 times not to use, 51-52 wired sprites, 478-479 movie coordinate system, 480 movie data atom, 155 movie data exchange components. See movie exporters; movie importers movie exporters alternative name for, 117 defined, 117 features, 117 settings, 123-126 See also exporting movies MovieExportSetSett i ngsFromAtomContai ner
function, 124 movie files creating. See QuickTime movie file creation defined, 250 media files and, 250 opening, 13-14, 249, 258, 259-261 movie importers alternative name for, 117 converting files into movies, 91-92 defined, 91, 117 example, 91-92 features, 117 movie information, 183-214 dialog boxes, 183, 184, 205, 206-208 movie previews, 191-195 overview, 183-186 See also file previews; movie annotations movie-in-movie capability. See embedding movies; movie tracks; picture-in-picture movies movie-in-movie communication, 530-532 movie-loaded events, 531-532 specifying targets, 530-531 movie-loaded events, 531-532
Index 6011
movie matrix, 480 movie media handler, 216 movie posters, 186-190 defined, 186 determining presence of, 190 elements defining, 186 illustrated, 184, 186 testing compression with, 390-391 movie poster time defined, 186 setting, to current movie time, 187-188 setting current movie time to, 188-189 movie poster tracks defined, 186-187 poster image determination, 190 working with, 189-190 movie previews, 191-195 clearing, 194-195 defined, 191 defining, 191-193 playing, 193-194 setting current movie selection to, 191-192 setting to current movie selection, 191 movie progress functions. See progress functions movieProgressUpdatePercent message, 136, 137, 138, 141, 142 movie property atoms, 532 movie rates nonlooping movies, 65 playback, 62-63 movie resource, 155, 156 movies actions targeting, 468-469 selecting entire, 36-38 See also QuickTime movie MovieSearchText function, 303-304, 306 MoviesTask function, 60-61, 66-67, 79, 80 movie targets finding, 518-522 retrieving names/IDs, 516-517, 523-526 specifying names/IDs, 512-516 targeting two external movies, 522-523
602
Index
movie time (current) getting, 309 pasted data and, 315 setting, to poster time, 188 setting poster time to, 187 text searches and, 302-304 movie time coordinate system, 153, 165-166 movie-to-movie communication, 511-526 controlling external movies, 522-523 external movie targets, 511-512 finding movie targets, 518-522 finding user data item, 515 retrieving target names/IDs, 516-517, 523-526 retrieving user data item value, 517 specifying target names/IDs, 512-516 targeting movie by name, 511-512 targeting two movies, 522-523 Movie Toolbox creating time base, 72 defined, 53 editing movies, 67-71 initializing, 56 playing movies, 65-67 playing open movies, 60-61 preparing for playback, 62-65 updating application framework, 59-61 See also specific Movie Toolbox functions movie tracks, 237-243 adding, to movies, 238-240 creating sample, 240-243 features, 237-238 movie user data, 38-48 cross-platform code, 345-347 getting stored window position, 46-48, 345-346 manipulating looping state, 43-46 overview, 38-39 specifying controller type, 39-42 movie windows attaching application-specific data to, 58-59 components, 2 creating window objects, 7-8
customizing types, 6 data change indicator, 6 expanding, 78-79, 315 first, finding, 9, 10 getting stored position, 46-48 graphics port association, 8 graphics vs., 94 handling, 6-11 illustrated, 3 invalidating, 317 maintaining information on, 6 open, iterating through, 9-11 QuickTime VR indicator, 7 redrawing, 315-317 resizing, 6, 15, 69, 94 subsequent, finding, 10, 11 types of, 6 without controllers, 54, 59-61 multi-image files, 105-106 multimatrix tweens, 459-462 atom structure, 459, 460 building media sample, 460-462 overview, 459 multiple document interface (MDI), 5 multiple platforms, 1
Ill namespace collisions, 351-352 native-endian format, 39 atom container data, 236 movie user data into, 346 reading/writing resource data, 347-350 from Resource Manager, 347 NavGetFile function, 127, 183, 196, 197, 198 Navigation Services replacement, 113 NewA1i as function, 249 NewAliasMinimal function, 249 NewCWindowfunction, 14 NewHandleClear function, 58, 295, 422 NewMovieControl let function, 22, 343,479 NewMovieEdi tState function, 68
new movie files. See QuickTime movie file creation NewMovieFromDataRef function, 249, 255, 259 NewMovieFromFi I e function alternative to, 249 handling imported data, 92, 292 loading movie data, 14 preventing calling, 93 NewMovieTrack function, 160, 164, 250, 254, 294, 450 NewRouti neDescri ptor function, 371 NewTrackMedia function, 160, 164-166, 250, 254, 294, 450 NewUserData function, 327 no-interface movie controller, 478-479 non-interleaved movie files, 159 nonlooping movies, 43, 66, 72 normal looping, 43, 71 determining presence of, 74-75 playback hints, 73 setting, 72, 73, 74 Normal Size menu item, 104
0 offscreen graphics world bit depth of, 425 for compression, 174, 377-378, 389, 390, 393, 394 creating, 174, 389, 390 drawing video frames and, 171, 173 pixel maps in, 175, 377-378 offset atom, 456-457 OpenADefaultComponent function, 48-50 OpenComponent function, 121 opening data handlers, 264 image files, 87, 90-92 movie files, 13-14, 249, 258, 259-261 resource files, 354-355 shortcut movie files, 226-227 URL data handlers, 264 URLs, 48-50, 259-261
Index 603
0penMovieFi ]e function, 13, 92 alternative to, 249 opening shortcut movies, 227 preventing calling, 93 open movies, playing, 60-61 Open URL menu item, 260 operand atoms, 490, 491,492, 493, 494, 523-525 operand targets, 526-530 operator atoms, 490 override frames, 407. See also sprite override samples
P palindrome looping, 43, 44, 71 determining presence of, 75 enabling, 72-73 playback hints, 73 setting, 73-74 parallel, adding in, 292, 315, 316 parent atoms, 230, 232-233 parent movies, 238 Pascal strings converting C strings to, 363-365 converting into handle, 210 converting to C strings, 365-366 copying text into, 209 preferred method, 366 specifications, 363 Paste menu item becoming "Add", 283, 284 becoming "Add Scaled", 284 becoming "Replace", 284 disabling, 283 pasting, 19 adding in parallel and, 292, 315, 316 current movie time and, 315 expanding movie box, 315 free memory amount, 545-547 imported text, 291-292 path tweens, 449 P-frames, 376
604
Index
PhotoShop format, 85 PICT format, 85 picture-in-picture movies, 76-83 border, 78-79, 80-81 display clipping region, 77-79, 82 drawing, 80-81 expanding movie box, 78-79 geometry, 76-77 moving, 81-83 overview, 76 playing, 79-81 preprerolling, 79 starting playing, 79 tasking, 79-80 user starting/stopping, 79 PictureViewer application exporting images, 107-112 Image menu, 86 QTGraphics application vs., 86-87 replicating functionality of, 86 pixel maps area of interest, 379 obtaining, 377-378 test image, 378-379, 390-391 playback preparing movies for, 62-65 preprerolling movies, 62, 63-65 prerolling movies, 62-63 rates, 62-63 playback hints, 73 playing movie previews, 193-194 playing movies idle-time tasks, 66, 79 Movie Toolbox, 65-67 starting, 65 P1ayMoviePrevi ew function, 193-194 PNG format, 85 PNG grayscale, 374, 375 pnot atom, 197-198, 199, 200, 201,204, 215, 216 pointer data handler, 261-262 component subtype, 247 uses, 247, 261 pointer data reference, 248, 261-262
pointer data reference record, 261-262 pop-up menu display chapter tracks, 277-279 handling user actions on, 33-36 modal dialog boxes, 359, 360-361 PopUpMenuSelect function, 33-34 porting layer, 351,367 PowerPC processors, 344 preprerolling movies, 63-65 completion routine, 63 defined, 62 picture-in-picture, 79 responding after, 64 PreprerollMovie function, 63, 64 prerolling movies, 62-63 PrerolIMovie function, 62, 63 preview atoms container atoms vs., 226 finding target of, 223-224 preview data atoms vs., 217 See also file previews preview data atoms finding, 223-224 preview atoms vs., 217 preview resource record, 197 previews. See file previews; movie previews programmable interactivity, 487 progress functions defaults, 133-134 dialog boxes, 133-134 for image operations, 147-148 operations triggering dialog box, 134 See also custom progress functions progress messages, 138-140 PtrAndHand function, 255, 258, 275, 296 PtrToHand function, 255, 296 PutMovieOnScrap function, 19
Q QTActionTargets application, 503-504. See also movie-to-movie communication; targeting; targets
QTApp_AdjustMenus function, 5, 104-105 QTApp_HandleContentCl ick function, 81 QTApp_HandleEvent function, 61,268, 269, 270 QTApp_HandleMenufunction, 60, 410 QTApp_Idle function, 61, 65-66, 67 QTApp_SetupWindowObject function, 56-57, 58 QTCmpr_ButtonProc function, 381-382 QTCmpr_Compresslmagefunction, 377, 383-385, 386 QTCmpr_CompressSequence function, 377, 388, 397, 398 QTCmpr_Instal l ExtendedProcs function, 382-383 QTCmpr_PromptUserForDiskFi I eAndSaveCompress ed function, 383 QTCompress application, 374, 376. See also compressing images; compressing image sequences; compression; standard image compression dialog component QTCopyAtomDataToHandle function, 233 QTCopyAtomDataToPtr function, 234 QTCountChi l dren0fType function, 233 QTCustom HandleCustomButtonC] i ck function, 33, 34-36 QTDataEx application, 149 QTDataRef application, 246. See also data handlers; data references QTDR_AddFil enamingExtens i on function, 273, 274 OTDR_C]oseDownHandlers function, 268, 269-270 QTOR_CopyRemoteFil eToLocal Fi le function, 270 OTDR_CreateReferenceCopy function, 250, 251, 252-253, 254 OTDR_GetMovieFromFi]e function, 249, 259 QTDR_GetMovieFromURLfunction, 259, 260 QTDR_GetURLBasenamefunction, 261 QTDR_GetURLFromUserfunction, 260 QTDR_MakeFil eDataRef function, 248, 253-254 QTDR_MakeURLDataReffunction, 259 QTDR_PIayMovieFromRAMfunction, 257 QTDR_TimerProc function, 269
Index 605
QTDR_WriteDataCompleti onProc function,
266-267, 269 QTDX_ExportMovieAsAnyTypeFi 1e function, 120-121, 149 QTDX_ExportMovieAsHintedMovi e function, 124-125 QTDX_Fil terFi les function, 127-128 QTDX_GetExporterSet t i ngs FromFi ] e function, 123 QTDX_ImageProgressProc function, 148 QTDX_ReadHandleFromFi 1e function, 124 QTDX_SaveExporterSetti ngs InFi ] e function, 124 QTFindChildByID function, 233, 235, 419 QTFindChi ldByIndex function, 233 QTFrame_AddComponentFi 1eTypes, 114 QTFrame_AdjustMenus function, 281-282, 285, 287 QTFrame ConvertMacToWinMenuItemLabel function, 288-289 QTFrame ConvertMacToWinRect function, 366-367 QTFrame_CreateWindowObject function, 7, 14 QTFrame_Fi ndExterna 1MovieTarget function, 519-522 QTFrame_GetAppDataFromWindow function, 58-59 QTFrame_GetFrontAppWi ndow function, 9 QTFrame_GetFrontMovi eWindow function, 521 QTFrame GetKeyboardModifiefs function, 287 QTFrame GetNextAppWindow function, 9, 10 QTFrame GetNextMovieWindow function, 521 QTFrame GetOneFiI eWithPrevi ew function, 369-371 QTFrame GetWindowObjectFromWindow function, 8-9 QTFrame HandleEdi tMenuItern function, 59-60 QTFrame HandleEvent function, 108 QTFrame_OpenMovieInWindow function, 56, 91, 93-94 QTFrame_PutFi le function, 161,263, 392 QTFrame SaveAsMovieFile function, 198 QTFrame_SaveAsMovie function, 179
606
Index
QTFrame SetMenultemState function, 13, 283, 289 QTFrame SetupControl I er function, 14 QTFrame_SizeWindowToMovie function, 59, 69, 94 QTFrame_StandardModal Dial ogEventFi I ter D
D
function, 108 QTFrame_UpdateMovi eFi ] e function, 198 QTGetAtomDataPtr function, 234, 235 QTGetNextChi ]dType function, 233 QTGraph_ExportImageFi I eDi aI ogEventFi I ter function, 108-109 QTGraph_ExportImageWithoutDi al og function, 111-112 QTOraphics application, 86-87. See also images QTInfo application, 185. See also file previews; movie annotations; movie posters; movie previews QTInfo ClearPreview function, 194-195 QTInfo CountAtomsOfTypelnFile function, 222, 223 QTInfo EditAnnotation function, 206, 213, 260-261 QTInfo_Fi ndAtomOfTypeAndlndexlnFi I e function, 219, 222, 223, 224 QTInfo_Fi ndPrevi ewAtomTarget function, 223-224 QTInfo_IsLastAtomInFi ]e function, 225 QTInfo_MakeFilePreview function, 200, 204, 215, 217, 219, 243 QTInfo_MovieHasPoster function, 190 QTInfo MovieHasPreview function, 192-193 QTInfoPlus application, 217. See also atom containers; file previews; movie tracks; shortcut movie files QTInfo_RemoveAl l Previ ewsFromFi l e function, 217-218, 219, 226 QTInfo_RemoveAtomFromFi ] e function, 224-225 QTInfo SetPosterToFrame function, 187 QTInfo_SetPreviewToSelection function, 191 QTInfo SetSelectionToPreview function, 191-192 QIInfo TextHandletoPString function, 209
QTInsertChild function, 232, 240, 418, 419, 483, 485 QTMakeMovie application, 160-161, 166, 179-180 QTMIM_AddMovieTrack function, 239 QTMIM_AddMovieTrackSampleToMedia function, 240, 241 QTML. See QuickTime Media Layer {QTML)
QTMM_AddVi deoSamplesToMedia function, 161, 167, 169, 174, 177, 240 QTMM_CreateVideoMoviefunction, 160-161, 168, 169, 180, 240, 250 QTMM_DrawFramefunction, 171, 173, 174 QTMooVToolbox application, 54-55. See also Movie Toolbox; specihc Movie Toolbox functions
QTMovieTrack application, 243 QTMT_DrawPicturelnPi ctureMovie function, 80-81
QTMT_IsMoviePal i ndromeLooping function, 75 QTMT_MoviePrePrerol l CompleteProc function, 64 QTMT_SetMovieCl ipRegion function, 77-78 QTMT_SetMovieLoopState function, 73-74, 79 QTMT_StartMovie function, 65 QTNewAIi as function, 248, 249 QTNewAtomContainer function, 232, 240 QTNextChi I dAnyTypefunction, 233 QTSetAtomData function, 418 QTShortCut CreateShortcutMovieFile function, 227, 229 QTShortCut Wri teHandleToFi I e function, 229 QTSprites AddlconMovieSamplesToMedia function, 420, 426-428 QTSprites_AddPenguinMovieSamplesToMedia
function, 426, 428-430
QTSprites AddPowerBookMovieSamplesYoMedia, 437-439 QTSprites AddTweenEntryTolnputMapfunction, 453-454 OTSpri tes AddVideoEntryToInputRap function, 443-444, 445,454
QTSprites application, 405-406. See also sprite animation; sprite atoms; sprite(s); sprite tracks QTSprites CreateSpritesMovie function, 411, 412, 413 QTSprites GetMovieSizefunction, 412-413 QTSprites HitTestSprites function, 431,432, 433-434 QTSprites_ImportVi deoTrack function, 439-441 QTSpritesPlus application, 436. See also D
tweening; tween tracks OTSprites SetTrackProperties function, 413, 475 QTSprites SetTrackProperties function, 413, 475 QTTarg_Addldl eEventVarTestAction function, 523 QTTC_DeleteTimeCodeTracks function, 340 QTTC_ShowCurrentTimeCode function, 337 QTTC_ShowStringToUser function, 337 QTTC_ShowTimeCodeSource function, 337-338 QTTC_ToggleTimeCodeDispl ay function, 339 QTText_AddTextTrack function, 296, 298-299, 540 QTText application, 281,282. See also chapter
tracks; text; text tracks OTText_EditText function, 308, 310 QXText_FindXext function, 304-306 QTText_IsHREFTRackfunction, 315 OTText_SetTextTrackAsHREFTrack function, 314, 315
QTTimeCode application, 320-321. See also timecode media handler; timecodes; timecode tracks QTURL_NewMovieFromURLfunction, 259-260 QTUti I s ChangeControllerType function,
41-42
QTUti I s ConvertCToPascalStri ng function, 365, 366 QTUti I s ConvertPascaIToCString function, 365 QTUti I s Del eteAl l ReferencesToTrack function, 340-341
Index 607
QTUti I s Fi ndUserDataltemWithPrefi x function, 514-515, 516 QTUti I s GetControllerType funtion, 39-40 QTUti I s GetMovieFileLoopinglnfo function, 43-44, 72 QTUtils GetMovieTargetIDfunction, 516, 521, 525 QTUti I s GetMovieTargetNamefunction, 516, 521,526 QIUt i ] s GetUserDataPrefi • ue function, 516-517 QTUti 1s GetUsersConnecti onSpeed function, 236 OTUti ] s 6etWi ndowPosi t i onFromFi ] e function, 46-47, 345-346 OIUti ] s HideContro] ]erBar function, 22-23 OTUti ] s HideContro] ]erButton function, 32 QTUti ] s PutContro] ]erBarOnTop function, 25-26 QTUtils SelectAllMovie function, 36, 37-38 QTUti ] s SetContro] ]erType function, 41 QYOti ] s SetMovieTargetName function, 513-514 OTUtils SetTrackName function, 314 OTUti 1s SetUsersConnecti onSpeed function, 237 OTUti 1s ShowContro] ]erBar function, 22, 23 OIUti 1s ShowContro] lerButton function, 31, 32 QTWiredActions application, 534-535. See also wired actions; wired text QTWired AddActi onsToSample function, 538-539, 540 QTWired AddActionsToTextMovie function, 540-544 QTWired AddCo] ] isionLogicToSprite function, 561 OTWired AddContro] ] erButtonSamp] estoMedi a function, 471,482 QTWired AddCornerCol] isionLogicToSprite function, 561 QTWired AddCursorChangeToSprite function, 499-500 m
D
608
Index
QTWired AddSideBounceToSprite function, 557, 558 QTWired_AddSpriteControl I erTrack function, 471,481-482 QTWired AddTransI ateByVariabl esActi on function, 554-556 QTWired CreateHyperTextActionContainer function, 544 QTWired_CreateMemoryDispI ayActi onContainer function, 544, 545, 548 QTWired_CreateMemoryDispl ayMovie function, 540 QTWired GetLowestLayerInMoviefunction, 478 QTWired MakeSpriteDraggable function, 495-498 QTWired SetTrackProperties function, 475, 476-477 QTWiredSpritesJr application, 465-466. See also draggable sprites; wired actions; wired sprites QTXP_DoLoopDia] ogEvent function, 362-363 QuickDraw, 8 QuickTime 3.0 overview, 344 QuickTime Media Layer {QTML}, 1,350-367 avoiding namespace collisions, 351-352 Carbon and. See Carbon converting data types, 366-367 features, 350-351 files, 352-354 initializing, 55-56 modal dialog boxes, 355-361 modeless dialog boxes, 361-363 overview, 344 resource files, 354-355 strings, 363-366 text, 367 unregistering applications from, 56 QuickTime movie defined, 152 structure, 152-154 tracks. See movie tracks; tracks QuickTime movie file creation, 160-168, 392-394 adding tracks, 164 m
code summary, 161-162 creating new movie file, 163 finishing up, 168 media samples. See media samples {adding) media segments, 167-168 media structure, 164 movie atom, 155, 156, 168 steps overview, 160-162 QuickTime movie file format, 151 QuickTime movie files creating. See QuickTime movie file creation defined, 152 double-fork. See double-fork movie files evolution of, 154 Fast Start, 156-157, 160 interleaved, 159, 160, 179 non-interleaved, 159 reference movie files. See reference movie files resource fork, 155-156, 168 saving, 179 self-contained, 158-159, 250 single-fork. See single-fork movie files structure of, 154-159 QuickTime Player application Edit Annotation dialog box and, 206, 209 emulating, 283-285 identifying as movie creator, 163 Import menu item, 127, 129 movie chapter access, 279 Open URL dialog box, 260 QuickTime VR basic interactions, 21 button constants, 30 control flags, 29-32 controller bar, 2, 4 determining movie type, 40 explicit flags, 29-32 managing buttons, 29-32 wired actions, 533-534
R RAM creating movie in, 255-257 playing movie stored in, 257 synchronous file transfers and, 265 rectangles controller bar, 26 converting Mac to Windows, 366-367 defining, 14-15 images, 99, 101 movie, 14-15, 26 picture-in-picture movies, 76-77 user item callback procedure, 359 reference extensions. See data reference extensions reference movie files creating, 250-254 defined, 157, 250 external references, 158 illustrated, 157 internal references, 158 media files in, 157-158, 250 saving space, 157 referring data, 248 region code, 209 registration point, 458 remote file copying local file to, 270-272 determining size of, 272 Remove Timecode Tracks menu item, 340 renamed functions, 351 Replace menu item, 284 resizing windows, 6, 15 resource data, reading/writing, 347-350 resource data handler, 257-258 component subtype, 247 limitations, 258 uses, 246, 257 resource data reference, 247-248 resource files, opening, 354-355 resource flippers, 347-350 resource fork, 154, 155-156, 168
index 6 0 9
Resource Manager cross-platform code, 347-350, 354-355 endian management, 347-350 QTML resource flippers, 348-349 reading/writing resource data and, 347-350 resource flippers, 347-350 resources (QTML), 354-355 restricting compressor types, 386 RotateMatrix function, 97, 457 rotating images, 100-102
$ sample boundaries, text, 308-310 sample description, 169-170 saving movies, 179 ScaleMatrix function, 97, 98 scaling images, 97-98, 102-105 scan code, 550 SCCompressImage function, 377, 383, 392 SCCompressSequenceBegin function, 394, 396 SCCompressSequenceEnd function, 396 SCCompressSequenceFrameAsync function, 398, 399-401 SCCompressSequenceFrame function, 396, 398, 400 SCGetlnfo function, 391 SCRequestImageSettings function, 377, 379, 383, 386 SCRequestSequenceSettings function, 386, 392 script editing, 544-545 Script Manager functions, 209, 211 SCSetlnfo function, 379, 386, 391 SCSetTestImagePixMap function, 377, 391 search flags, 302-303, 304 searching backward, 309 for HREF track, 279, 298 for starting time, 308-309 for text, 277, 302-306 Select All item, 36-38 self-contained movie files, 158-159, 250
610
Index
SetDi alogl tem function, 359 Setldenti fyMatrix function, 97
SetMediaInputMap function, 445, 454 SetMedi aPropertyAtom function, 424 SetMode] essDi a ] ogCa] ] backProc function, 362 SetMovieBox function, 188 SetMovieDi spl ayCl ipRgn function, 77 SetMovieP] ayHints function, 73 SetMoviePosterTime function, 187, 188 SetMoviePreferredRate function, 63 SetMovi ePrevi ewModefunction, 194 SetMoviePreviewTime function, 191, 194 SetMovieProgressProc function, 134-135, 137 SetMovieRate function, 65, 388 SetMovieSelection function, 67-68 SetMovieTime function, 63 Set Preview to Selection menu item, 191, 192 Set Selection to Preview menu item, 191, 192 SetIimeBaseF] ags function, 72 SetTrackEnabled function, 298, 339 SetZrackUsage function, 189 SetOserDataItem function, 40-41,205-206 SetWi ndowLong function, 7 5etWRefCon function, 7 shortcut movie files, 226-229 creating, 227-229 defined, 226 features, 226, 227 opening, 226-227 structure of, 227 Show Copyright, 183, 185 ShowMovielnformation function, 186, 205 ShowWindow function, 351 single-fork movie files, 154, 155-157 converting double-fork into, 158 Fast Start movie files vs., 157 file previews, 197-204 "flattening" and, 158-159 structure of, 155-156 SMPTE timecodes, 319, 321-323 Society of Motion Picture and Television Engineers (SMPTE), 319 sound media time scale, 165 spatial compression, 376-377
Speaker button. See Volume control button spin tweening, 457-459 building media sample, 457-458 matrix, 458-459 output, 458 registration point, 458 See also matrix tweening; multimatrix tweens sprite animation cel animation vs., 405 defined, 403 reducing file size, 405 sprite atoms adding, to key frame sample, 413-414 adding, to sample container, 419 children, 407-409, 413-414, 418, 419, 424 IDs, 407, 409 media property, 424 setting sprite images, 419-421 structure of, 407-409 sprite button behaviors, 498-500 atom data, 499 attaching, 499 custom cursor, 498, 499-500 defined, 499 sprite graphics mode, 406-407 sprite image arrays, 407 sprite image index, 406, 414, 486 sprite index setting action atom, 486 sprite key frame samples adding, to sprite media, 421-423, 437-439 adding atoms to, 413-414 adding compressed image data to, 420-421 format, 407-409 sprite layer, 406, 414 sprite matrix, 406 Spri teMedi aHi tTestAl 1Spri tes function, 431-432 Spri teMedi aHi tTest0neSpri te function, 431 sprite movies adding video override to, 445-447 adding video track to, 439-441 creating, 410-413 getting size of, 412-413
wired, 160 See also sprite tracks
sprite override samples creating samples, 423-424 defined, 407 format, 409, 410 interpretation modes, 425-426 sprite{s), 403-434 acting like buttons, 466-468, 471-472 atom structure, 408 bouncing. ,See bouncing sprites changing image association, 403-404 collisions, 556-562 compressed data, 420-421,422, 426 defined, 403 draggable. See draggable sprites enabling/disabling movie track, 509-510 external, image index, 527-529 Go-To-Start, 471-475, 476 hit testing, 430-434 horizontal position, 473 IDs, 407, 409, 431 images, setting, 419-421 image sources, 407 media property atoms, 475 moving, 553-556 override frames, 407 override samples. See sprite override samples overview, 403-406 properties, 406-407, 413-419, 471-473 sample description, 421-422 spin tweening and, 457-459 targeting, 504-506 track rectangle collisions, 556-560 translating, using variables, 554-556 video override tracks and. See video override tracks wired. See wired actions; wired sprites sprite tracks actions on, 469 adding key frame sample to media, 421-423, 437-439 adding samples to icon movie, 426-428
Index 611
sprite tracks (continued] adding samples to penguin movie, 428-430 building, 437-439, 470-473 button behaviors, 498-500 creating, 410-413 key frame samples format, 407-409 layers of, 477-478 media property atoms, 424 media sample types, 407 override samples. See sprite override samples properties of, 424-426, 475-477 setting sprite images, 419-421 setting sprite properties, 413-419, 471-473 shared data structure, 409 track matrix, 480-481 variables, 487-488, 492, 558-560 video override tracks and. See video override tracks for wired movie, 470-473 sprite track variables, 487-488 Spri teUti I s_AddCompressedImageToKeyFrameSample function, 421 Spri t eUt i I s AddPI CTImageToKeyFrameSample
function, 420, 421 Spri teUt i ] s AddSpriteSamp] eToMedia function, 423 Spri teUti ] s_AddSpri teToSamp]e function, 418-419 SpriteUtils SetSpriteOata function, 414, 415, 418 sprite visibility state, 406, 414 Standard File Package, 128, 352, 369 D
StandardGetFi I ePrevi ew function
Carbon-supported alternatives, 369-371 file previews, 183, 196, 197-198 filtering movies and, 127, 128 finding first pnot atom, 204 finding image files, 112, 113 importing and, 129, 132-133 standard image compression dialog box configuring, 389-390 filtering events in, 380-381
612
Index
hook functions, 381-382 for image sequences, 386-387, 389-390, 391-392 installing extended procedures, 382-383 introduced, 373, 374 standard image compression dialog component configuring, 389-390 defined, 373 features, 373-374 overview, 373-375 restricting compressor types, 386 See also compressing images; compressing image sequences; standard image compression dialog box starting movies before full download, 157 picture-in-picture, 79 StartMovie function, 62, 63, 65, 66, 194 Step Backward button, 28 Step Forward button, 28 still images, 92-95 Stop button, 134, 140, 141-142 StopMovie function, 194 stopping movie, 388 strings cross-platform code, 363-366 preferred method for, 366 types of, 363 See also C strings; Pascal strings support attaching movie controller, 14-17 editing. See editing events, 17-18 opening movie files, 13-14 synchronous file transfers, 264-265
11" target data, 248 targeting external movies. See movie-to-movie communication
movies by name, 511-512 sprites, 504-506 tracks, 506-510 See also targets targets, 503-532 default, 503, 504 defined, 466 external. See movie-to-movie communication movie-in-movie, 530-531 operand, 526-530 overview, 503-504 See also targeting target tracks, 310 tasking movies defined, 60 nonlooping movies, 66 passing NULLvalue, 80 picture-in-picture, 79-80 TCGetCurrentTimeCode function, 337, 338 TCGetDispl ayOptions function, 330 TCSetSourceRef function, 327-328 TCSetTimeCodeFlags function, 339 TCTimeCodeToFrameNumber function, 329, 330 TCTimeCodeToString function, 337 television timecode standards, 321-323 temporal compression, 376-377 Termi nateQTMLfunction, 56 test image compressing images, 378-379 compressing image sequences, 390-391 pixel maps, 377-378 setting, 378-379, 390-391 Test menus QTActionTargets application, 503-504 QTCompress application, 374, 375 QTDataRef application, 246 QTInfo application, 185 QTMooVToolbox application, 55 QTSprites application, 405, 406 QTSpritesPlus application, 436 QTText application, 281,282 QTTimeCode application, 320-321
QTWiredActions application, 534, 535 QTWiredSpritesJr application, 465-466 text from clipboard, 291-292 cross-platform code, 367 from files, 292-294 highlighting, 303 importing, 291-294 overview, 277-281 wired. See text actions; wired actions text actions, 535-549 adding, to text sample, 536-539 creating, 539-547 hypertext, 535, 536, 537, 547-549 on partial text, 536 pasting free memory amount, 545-547 uses, 535-536 text callback procedure, 307 TextDescri pti on structure, 295 text description structures, 294-295 text descriptors, 292, 294 Text Import Settings dialog box, 292, 293 TextMedi aAddTextSample function, 297, 306, 310, 311 TextMediaFindNextText function, 302, 303, 304, 306 TextMediaHi I i teTextSample function, 303 TextMediaSetTextProc procedure, 301,307 text movie import component, 291
text options structure, 330 text tracks, 294-301 adding text media samples, 294-297, 306 creating, 298-301 disabling, 298 editing text, 306-310, 544-545 enabling, 298 enabling editing on, 544-545 finding sample boundaries, 308-310 getting current text, 307-308 hiding, 298 illustrated, 278 introduction, 277 keyed text, 277, 278 location, 277
Index
613
text tracks [continued] moving time forward/backward in, 277, 309 pasting free memory amount into, 545-547 positioning, 297-298 searching for text, 277, 302-306 setting track matrix, 298 text box characteristics, 295 time searches, 308-309 wired actions. See text actions See also chapter tracks textures, 51-52 thumbnails, 201-202, 204, 373, 377, 379 TIFF format, 85 time remaining, 139-140 specifying location, 36-38 starting, finding, 308-309 tracking, of image sequences, 388 units, 153 time base, 72, 238 time base control flags, 72 timecode counter record, 329 timecode definition structure, 326-327 timecode media handler features, 319, 323 finding, 323-324 overview, 319 settings dialog box, 324, 325 Timecode Options dialog box, 325, 331 timecode records, 329-330 timecodes, 319- 342 clock characteristics, 319, 320 displaying current value, 337 drop-frame, 319, 322 origin, 319 overview, 319-321 precision of, 321-322 sample description, 325-328 simple counters and, 323, 326-327 standards, 319, 321-323 See also timecode tracks timecode time record, 329, 330
614
Index
timecode tracks, 324-342 adding, 324-328 deleting, 340-342 hiding, 338-340 media samples, 328-330 operations, 336-342 showing, 338-340 source information, 327-328, 337-338 timecode sample description, 325-328 toggling visibility, 339-340 track geometry, 330-332 track references, 310-311,332-336 timecoding, defined, 319 time coordinate systems media, 165, 166 movie, 153, 165-166 timer callbacks, 269 time scale default, 165-166 defined, 37, 153 example, 153 finding time values in, 308-310 media, 165 sound media, 165 specifying, 165 track offset and, 153 video media, 165 See also timecodes; timecode tracks time units, 153 track layer, 477-478 track matrix, 480-481 track offset, 153 track rectangle collisions, 556-560 track references creating, 332-336 defined, 310 graphics mode tween, 453 types of, 310-311,441-442 video override tracks, 441-442 tracks actions on, 469 adding, 164 adding media to, 164-166 coordinate system, 479-480
defined, 152 dimensions of, 164 inserting, 251,439 media segments into, 167-168 media structure of. See media (structurel movie poster, 186-187, 189-190 QuickTime movie structure and, 153-154 sprites enabling/disabling, 509-510 structure of, 153 targeting, 506-510 volume level, 164 See also movie tracks; specific track types track usage, 189-190, 194 'l't'ansfer Remote File menu item, 270. See also file transfers TranslateMatrix function, 97, 98, 494, 495 ~'im menu item, 283, 284 tween data atoms, 451 building, 455-456 duration atom, 457 multimatrix, 459-460 offset atom, 456-457 tween entry, 450 tweening data atoms, 451,455-457, 459-460 data types, 448-449 defined, 435 duration atom, 457 graphics mode, 449-455 matrix, 455-457 media sample structure, 450-451 multimatrix, 459-462 offset atom, 456-457 overview, 447-449 spin, 457-459 tracks, 435, 450-453, 455 tween media handler, 447, 448, 452, 455, 462 tween offset atom, 456-457 tween tracks advantages, 455 defined, 435 features, 435 frame rate, 455
graphics mode tween, 450-453 override samples vs., 455 See also tweening
[[j Undo menu command, 20 universal procedure pointer (UPP), 108, 371 updating application framework, 59-61 UPP. See universal procedure pointer (UPP) URL data handlers, 245, 258-261 component subtype, 247 configuring, 264 for file transfers. See file transfers opening, 264 opening movie files, 258, 259-261 types of, 259 uses, 246, 258 See also URL data references URL data references creating, 259 defined, 248 referring data, 248 target data, 248 URLs basenames of, 261 defined, 258 dialog boxes for, 259-261 HREF tracks and, 279, 298, 314-315 opening, 48-50, 259-261 schemes, protocols of, 258-259 wired action parameter, 470 wired actions. See wired actions user data. See movie user data user item callback procedure, 358-359
V validating image data, 88-89 variable IDs, 487, 492
Index 615
variables concatenating, 488 floating-point, 487-488 setting, 487-488 sprite track, 487-488, 492, 558-560 video frames adding, to media, 176-179 compressing, 173-176 drawing, 170-173 video override tracks, 436-447 adding, to sprite movies, 445-447 building sprite track and, 437-439 features, 435 input map, 442-447 movie structure, 439 overview, 435, 436 track reference, 441-442 tween tracks vs., 455 video tracks adding, to sprite movie, 439-441 copying, from movie to movie, 439-441 virtual key code, 550 Volume control button displaying without sound track, 30-31 QuickTime VR controller, 29, 30-31 standard controller, 28 volume level, 164
W-Z WaitNextEvent function, 17 Web browser, launching, 48 while atoms, 488-489 window object records, 6 creating window objects, 7-8 retrieving window-specific data from, 8-9 window objects attaching application-specific data to, 58 creating, 7-8 setting up, 57-58 windows. See graphics windows; movie windows
616
index
Windows platform event-driven programming, 4-5 keyboard modifiers, 283-284, 285-287 renaming Edit menu items, 287-289 See also cross-platform code; QuickTime Media Layer (QTML) wired actions, 463-501,533-563 controlling processing of, 488-490 defined, 280, 463 events, 280, 466-468 examples, 280, 281,463-465 features, 463-466, 533-535 illustrated, 281 mouse button triggering, 280, 466-468 overview, 463-466 parameters, 470 specifying, 468-470 targets. See movie-to-movie communication; targets for text. See text actions See also draggable sprites; wired sprites wired action targets. See targeting; targets wired sprites, 160, 470-482 acting like buttons, 466-468, 471-472 action parameter atom, 485 adding action atoms, 484-485 building track for, 470-473 combined code, 481-482 coordinate systems, 479-480 defined, 405, 466 draggable sprites. See draggable sprites event atoms, 466, 467, 483-484, 492 events triggering, 466-468 external movies and. See movie-to-movie communication movie controllers, 478-479 track layers, 477-478 track matrix, 480-481 track properties, 475-477 variables, 487-488 wiring actions to sprites, 473-475 See also draggable sprites; wired actions
wired sprite utilities, 482-487 adding action atoms, 484, 485 adding action parameter atom, 485 adding event atoms, 483-484, 485 adding sprite index setting action atom, 486 features, 482-483 final code, 486-487 wired text. See text actions WiredUti I s AddActionAtom function, 484, 485 WiredUti 1s AddActionParameterAtom function, 485 WiredUti 1s AddMovieIDActionTargetAtom function, 512 WiredUti ] s AddMovieNameActi onTargetAtom function, 511-512, 523 m
WiredUt i I s_AddQTEventAndActi onAtoms
function, 485, 557 WiredUti 1s_AddQTEventAtom function, 483-484, 485 WiredUti 1s AddSpri teIDActionTargetAtom function, 505, 506 WiredUt i ] s_AddSpri teSet Image I ndexAct i on function, 486 WiredUti l s AddTrackNameActionTargetAtom function, 507 WiredUti 1s AddTrackTargetAtom function, 507-509 See also specific utility names
Index 617
This Page Intentionally Left Blank
A b o u t the CD-ROM
The accompanying CD-ROM contains the source code and project files for the applications described in the books Q uickTime Toolkit, Volume One: Basic Movie Playback and Media Types and QuickTime Toolkit, Volume Two: Advanced Movie Playback and Media Types. Look in the folders Volume One or Volume Two for the materials appropriate for the book you are reading. Inside each of these folders, the materials are grouped by chapter. Note that a few chapters do not have associated sample applications. On Windows, you can open the .mak project files using Microsoft Visual Studio. On Macintosh, you can use Metrowerks CodeWarrior IDE to open the .mcp project files. The Extras folder for each book also contains an Xcode project for the Carbon version of QTShell, which forms the basis of all the other applications. You may need to adjust the access paths specified in the project files. Here is the folder arrangement assumed in all the projects supplied:
619
QuickTimeSDK MacSampleCode qtshell qtcontrollerfun {etc.} QTDevMac CIncludes Libraries RIncludes QTDevWin CIncludes Libraries RIncludes Tools You can download the latest QuickTime software development kit from
http: //developer. apple, com/quic ktime/. The use of the source code is governed by the following license agreement.
License Agreement 9 2004, Apple Computer, Inc. All rights reserved. This Apple software is supplied to you by Apple Computer, Inc. {"Apple"} in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software {the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms, provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Computer, Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights
620
About the CD-ROM
or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES {INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION} ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT {INCLUDING NEGLIGENCE}, STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Publisher's Limited W a r r a n t y The Publisher warrants the media on which the software is furnished to be free from defects in materials and workmanship under normal use for 30 days from the date that you obtain the Product. The warranty set forth above is the exclusive warranty pertaining to the Product, and the Publisher and MacTech disclaim all other warranties, express or implied, including, but not limited to, implied warranties of merchantability and fitness for a particular purpose, even if advised of the possibility of such purpose. Some jurisdictions do not allow limitations on an implied warranty's duration, therefore the above limitations may not apply to you.
Publisher's Limitation of Liability Your exclusive remedy for breach of this warranty will be the repair or replacement of the Product at no charge to you or the refund of the applicable purchase price paid upon the return of the Product, as determined by the Publisher in its discretion. In no event will the Publisher or MacTech or their directors, officers, employees, or agents, or anyone else who has been involved in the creation, production, or delivery of this software be liable for indirect, special, consequential, or exemplary damages, including, without limitation, for lost profits, business interruption, lost or damaged data, or loss of goodwill, even if the Publisher, or an authorized dealer or distributor
About the CD-ROM 621
or supplier has been advised of the possibility of such damages. Some jurisdictions do not allow the exclusion or limitation of indirect, special, consequential, or exemplary damages or the limitation of liability to specified amounts, therefore the above limitations or exclusions may not apply to you.
622
About the CD-ROM