Video the Way Your "Ancestors" Intended
December 27, 2004
by Chuck Neal
One of the primary strengths of Macromedia Director is the various media types it can combine in any application. Director can utilize most any standard graphic format, audio format, and almost every popular video technology on the market. If there is not a current media type supported Director can easily expand its feature set with an Xtra, making it infinitely expandable. This allows for amazing flexibility, but all these different formats can create some authoring hassles.
Video formats vary in capabilities, quality, and size. There is no one universal format that seems to outweigh all others for every use. Flash video is wonderful for web delivery, and for smaller files. If you need a perfect quality video for a kiosk; MPEG-2 or MPEG-4 may suit your needs. QuickTime is great for CD delivery, but there is always the hassle of having to install applications on user's machines. Each has their uses but this can create some complexity for developers when trying to author with multiple formats in mind. There is also the inevitable occurrence of a late project video switch that causes some recoding to adapt to a new format. No one wants to be up late the night before a large project is due trying to rebuild everything for the switch, but is there an easier way to handle this?
Well, since we have gone this far, you can probably guess that I am going to say yes, and also explain my approach for handling this type of problem. As with any solution this approach will require some up front planning, but the benefits can be reaped many times over. You also get the benefit of taking what I have already written and expand on it, rather than starting completely from scratch. Here is the thought process that I went through to streamline video playback so that I would almost never have to deal with rebuilding the same old video controller again.
My first rule of programming is simply this. "If you have to do it more than once, there is probably an easier way." Any time you write a new script or behavior look for anything that can be reused. If you make a behavior to control a button, write it so that it can be reused for any type of button in the future. The basic concept can apply here as well. When constructing our video controller we need to look at ways to make it as flexible as possible. This means that we don't want to commit some common mistakes like refering to specific sprite numbers, frames, or requiring some exact order for elements to be aligned in the score. This should be as flexible as possible so any part of the system can be removed and it can still function.
The next step is to look at the scope of what we are building. We are creating a video controller, but we already know that this will need to support multiple formats. There are 3 ways we could easily handle this, each with their pros and cons.
1. Write separate behaviors for each media type. - This can make editing code simple for each type but also creates a lot of unnecessary duplication in the code. If you want to change core functionality then you have to edit each behavior seperately. Lets say we decide one day that we want the rewind button to run on mouseUp until we click play instead of while its being held down. If we have six video formats that's six chunks of code that have to be rewritten. This is not ideal at all.
2. Write one big behavior. - This is definitely more elegant, as we have one source to work from. In many cases I would recommend this route as it keeps all the necessary code in one place. An early approach to this behavior that I wrote years ago combined the code for the video, buttons, slider, etc., all into one behavior. It's handy to not have to keep up with the separate pieces. But when dealing with this many formats you can quickly end up with long chunks of code in a rat's nest of "if" and "case" statements. It may be nice and compact, but debugging is still a bit harder than it needs to be.
3. Centralize all common code and split off only the parts that vary. - This approach is what I will demonstrate here. The idea is to carefully plan your code so that the core behavior will handle all the general tasks, then you only split up code that is specific to a particular media type. We will accomplish this through the use of "ancestors".
So what is an ancestor? It doesn't mean you have to track your code's genealogy, but the same basic concept applies. Here is a brief example of how an ancestor can be used.
Lets say we have two animals. A cat and a dog. Both are animals, both have legs, but some properties vary. Lets start with a simple script for the general "animal."
Property pSound, pHasFur
On new me
PHasFur = 1
Return me
End
On putSound me
Put pSound
End
Now lets create a "cat" script.
On new me
Me.ancestor = script("animal").new()
Me.pSound = "meow"
End
And a "dog" script.
On new me
Me.ancestor = script("animal").new()
Me.pSound = "arf"
End
Now lets create one of each.
Cat = script("cat").new()
Cat.putSound()
--"meow"
And a dog.
Dog = script("dog").new()
Dog.putSound()
--"arf"
With this in mind, there are two ways we can approach our new controller.
1. Create one ancestor for each new controller type.
2. Create one script that dynamically selects an ancestor.
Either way is perfectly fine, but for my uses I think the second option will be better. With this approach the ancestor is chosen based on the type of media it finds. Because the core behavior is the same, we can swop media on a sprite over and over again without ever having to change the behavior attached to it. This allows us to focus more on the project at hand, and worry less about what type of media we just placed on screen.
The first part of the code we need is the video controller. This will be placed on the video sprite and handles all the interaction. The behavior has a few key tasks.
1. Determine what type of media we are using
2. Attach the appropriate ancestor
3. Handle all video events from buttons (play, pause, seek)
4. Track base properties of the video.
Here is the beginSprite handler.
on beginSprite me
--get the member reference
pMember = sprite(spriteNum).member
--get the type
pType = pMember.type
--qt and digital video use the same controller
if pType = #digitalVideo then pType = #quickTimeMedia
--make sure we have a parent script for this type
if me.memberExists(pType && "Video Base") then
--set the ancestor
me.ancestor = script(pType && "Video Base").new()
--make it active
pActive = 1
else
--no parent so deactivate it
pACtive = 0
exit
end if
--container for general settings. Varies for each part
pInfo = [:]
--prep it
pInfo[#baseTempo] = the frameTempo
--call the ancestor to prepare the video
me.prepVideo()
--start it playing
me.doVideo(#play)
end
The first thing we do is to establish the type of media we are going to be utilizing. Using that we can check and see if we have an available script for that type and attach it. As long as we name the future scripts in the same manner we can continue to add new media types without ever touching the base code again. This becomes infinitely scalable with no hassles for existing media types.
We then grab the Director movie's tempo (it is used by some media types to calculate rewind and FF intervals) and run a prepVideo() handler. This is the first example of a handler that does not reside anywhere in the base script. PrepVideo becomes a handler we place in any new media type script to initialize and setup calls for that video. For QuickTime it might be for turning on Direct-to-Stage. For Flash we may want to calculate its playback speed/rate. We look for any common places that we may need to interact with the media and create these universal calls to adapt for each new type.
The last item we run is the doVideo() command. This is our base script handler to interpret events from buttons. We run this with the #play property to trigger the video to start up immediately.
Lets take a quick look at the doVideo() handler to see how we handle each event, then we will look at how two specific media types vary.
on doVideo me, vItem, vParam
if not pActive then exit
case vItem of
#play :
--slow director
me.slowDirector()
--play it
me.playVideo()
If you look at the beginSprite handler you will see that we disable the script if a valid ancestor is not found. This prevents errors on unrecognized types. We check this at the start of the doVideo command and exit if we are not active. Next we look at what command was sent. In this case we focus on the play command. The slowDirector call is an option I have written into my code to allow Direction to slow its frame rate and allow the video to claim more of the CPU if necessary. Next we call the playVideo() call to the ancestor to let it start the video. Lets look at the Flash and QuickTime versions of the ancestor scripts to see how they vary for this command.
Flash
on playVideo me
sprite(me.spriteNum).fixedRate = me.pInfo[#playRate]
sprite(me.spriteNum).play()
end
QuickTime
on playVideo me
sprite(me.spriteNum).movieRate = 1
end
Both trigger the video to start, but each has a different approach. We now have the ability to play back two media types from the same behavior, and can keep adding more. Lets add another popular type- MPEG. MPEG Advance Xtra isa very popular solution for this, so here is its variant of this handler.>/P>
on playVideo me
sprite(me.spriteNum).rate = 1
sprite(me.spriteNum).play()
end
If we tried to create one large behavior for this we would have a mass of "if" and "case" statements for every command. This can make debugging difficult and gets more complex as we start finding larger and larger variations in how some media types handle different functions. Here is an example comparing QuickTime and Flash for rewinding.
QuickTime allows rewinding by simply setting the movieRate of the sprite to -2. Flash does not allow reverse playback so we instead have to fake it by calculating the time elapsed each frame and reversing the video by 2 times that amount. Here are the 2 scripts.
QuickTime
on rewindVideo me
sprite(me.spriteNum).movieRate = -2
end
Flash
on rewindVideo me
sprite(me.spriteNum).pause()
--grab the time to fake the rewind
me.pInfo[#lastTime] = the milliseconds
end
on rewindStepVideo me
--Fake Rewind since MPEG Advance does not support it
t = the milliseconds - me.pInfo[#lastTime]
--backup 2X this ammount
sprite(me.spriteNum).seek(sprite(me.spriteNum).currentTIme - (2 * t))
me.pInfo[#lastTime] = the milliseconds
end
Flash needs to check this every frame, while QT simply runs until we change the rate back. These differences would make a single script very unmanageable, while splitting the pieces into separate ancestors make this a very simple and elegant process.
After we code for play, pause, rewind, seek, etc., we need to make one more behavior for all the buttons. Since these buttons all address our core behavior they do not need any changes for each media type either. You can either communicate with scripts directly, use a sendSprite, or a sendAllSprites call. Here is a short overview of the differences.
1. object.function() - This approach calls a function on a specific object, or sprite. This can be handy as it runs fast, but if there is any problem it will throw and error and stop your program.
2. sendSprite - This is a step up from calling the object directly. It sends a message to a specific sprite, but if the handler is not on that sprite it's simply ignored. The down side is you have to know where everything is. There are many ways to handle this up front, but I find for systems where speed is not an issue, there is another approach.
3. SendAllSprites - This command broadcasts a message out to all sprites. If the sprite has the handler it runs it, otherwise its ignored. This can be a little slower (a very few milliseconds at most) but for a flexible system like this I find it to be very handy. You can establish buttons that span multiple frames and move media to different channels without worry of any piece loosing sight of the rest of the system.
Here is an example of a portion of the button handler that sends out the play command.
on mouseUp me
case pWhatItem of
#play, #pause, #stop :
--play, pause or stop was clicked
sendAllSprites(#doVideo, pWhatItem)
When we click the button it simply tells the sprite(s) with a doVideo command to run the specified command. This makes it very fast and easy to deploy solutions that will not break by moving frames or changing sprite orders.
You can download the example movie that demonstrates how to use this approach for QuickTime, Flash, and MPEG Advance. I have also utilized this in various portions for Real Media, Windows Media, LDMs (Linked Director Movies), Audio, or other video Xtras. Creating a new media type is as simple as copying another ancestor and adjusting the few lines of code that handle the video playback. The rest is already done for you and never has to be re-written again.
Ancestors are a powerful way to reuse and simplify complex systems in lingo. Give them a try and you will see why it's great to be kind to your ancestors.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.