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 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 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 O) && (theOperation 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) 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 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 (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 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
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 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) h v 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