﻿// -----------------------------------------------------------------------
//  Copyright (C) Microsoft Corporation. All rights reserved.
// -----------------------------------------------------------------------
//  SilverlightMedia.js
//  MediaPlayer component
Type.registerNamespace('Sys.UI.Silverlight');

Sys.UI.Silverlight._DomElement = function(element, visible) {
    // <summary>Represents a Silverlight element.</summary>
    // <param name="element">The Silverlight element.</param>
    // <param name="visible" type="Boolean">Specifies whether the element is expected to be initially visible.</param>
    this._element = element;
    this._visible = !!visible;
    this._bindAutoAnimations(element, element.Name);
}
Sys.UI.Silverlight._DomElement.prototype = {
    _events: null,
    _animations: null,
    _enabled: true,
    _mouseOver: false,
    
    get_element: function() {
        // <value>The Silverlight element</value>
        return this._element;
    },

    get_enabled: function() {
        // <value type="Boolean">Determines whether the DomElement responds to Silverlight events.</value>
        return this._enabled;
    },
    set_enabled: function(value) {
        if (value !== this.get_enabled()) {
            // e.g. might make the element look grayed out or increase its alpha
            this._enabled = value;
            this._play(value ? "enable" : "disable");
            // If element becomes disabled while the mouse is over
            // play the mouse_leave animation as well, otherwise the element remains
            // stuck in the mouse_enter state.
            if (!value && this._mouseOver) {
                this._play("leave");
                this._mouseOver = false;
            }
        }
    },
    
    get_visible: function() {
        // <value type="Boolean">Toggles the Silverlight element's visibility.</value>
        return this._visible;
    },
    set_visible: function(value) {
        if (value !== this.get_visible()) {
            this._visible = value;
            if (!this._play(value ? "show" : "hide")) {
                // if no show/hide animation then directly set the element visibility
                this.get_element().visibility = value ? 0 : 1;
            }
        }
    },    
    
    _bindAutoAnimations: function(element, name) {
        this._animations = {
            "show": element.findName(name + "_Show"),
            "hide": element.findName(name + "_Hide"),
            "enable": element.findName(name + "_Enable"),
            "disable": element.findName(name + "_Disable"),
            "leave": element.findName(name + "_MouseLeave")
        }
        if (this._animations["leave"]) {
            // no need to hook these events if there's no MouseLeave event
            // we only hook these to play
            // _MouseLeave when the element becomes disabled.
            this.bindEvent("mouseEnter", name + "_MouseEnter", this._onEnter);
            this.bindEvent("mouseLeave", name + "_MouseLeave", this._onLeave);
        }
    },
    
    bindEvent: function(eventName, animationName, callback, callbackOwner) {
        // helper that reduces code necessary to bind a silverlight event to a function
        // and have it play an animation by name
        // if get_enabled() is false, animations don't play and callbacks aren't called.
        // For example, a disabled button does nothing even though a MouseEnter
        // storyboard exists for it.
        var element = this.get_element();
        var animation = null;
        if (animationName) {
            animation = element.findName(animationName);
        }
        // no animation found and theres no callback, no reason to hook the event
        if (!animation && !callback) return;
        
        if (callback) {
            callback = Function.createDelegate(callbackOwner || this, callback);
        }
        
        var handler = this._createEventHandler(animation, callback);
        var token = element.addEventListener(eventName, handler);
        
        // save token in _events so we can automatically removeEventListener in dispose
        if (!this._events) {
            this._events = [];
        }
        // handler is not actually needed because only the token is used to remove the event
        // but access to the handler is needed by some QA automation in order to simulate
        // actual silverlight UI events like mouseEnter.
        this._events[this._events.length] = { eventName: eventName, token: token, handler: handler };
    },
    
    _createEventHandler: function(animation, callback) {
        return Function.createDelegate(this,
            function(sender, args) {
                if (!this.get_enabled()) return;
                // if callback exists, it can return false to cancel animation if any
                if (callback && !callback(sender, args)) return; 
                if (animation) {
                    animation.begin();
                }
            });
    },
    
    dispose: function() {
        if (this._events) {
            var element = this.get_element();
            for (var i = 0, l = this._events.length; i < l; i++) {
                var e = this._events[i];
                element.removeEventListener(e.eventName, e.token);
            }
            this._events = null;
        }
        this._animations = null;
        this._element = null;
    },
    
    _onEnter: function() {
        this._mouseOver = true;
        return true;
    },
    
    _onLeave: function() {
        this._mouseOver = false;
        return true;
    },

    _play: function(name) {
        var a = this._animations[name];
        if (a) {
            a.begin();
            return true;
        }
        return false;
    }
}
Sys.UI.Silverlight._DomElement.registerClass('Sys.UI.Silverlight._DomElement', null, Sys.IDisposable);

Sys.UI.Silverlight._Button = function(element, visible, repeatInterval, clickCallback,
                                        doubleClickCallback, callbackOwner, states) {
    // <summary>
    // Makes a Silverlight element behave like a button. Clicking it calls the click callback. Double clicking it calls
    // the double click callback. If given, repeats the click event continuously while pressed. If a state list is given,
    // supports a mutli-state button where each click toggles it's state.
    // </summary>
    Sys.UI.Silverlight._Button.initializeBase(this, [element, visible]);
    element.cursor = "Hand";
    this._repeatInterval = repeatInterval;
    this._clickDelegate = clickCallback ? Function.createDelegate(callbackOwner, clickCallback) : null;
    this._doubleClickDelegate = doubleClickCallback ? Function.createDelegate(callbackOwner, doubleClickCallback) : null;
    
    this._elements = [];
    if (states) {
        // button has child states
        for (var i = 0; i < states.length; i++) {
            var e = element.findName(states[i]);
            // States may not actually exist in the UI and are optional.
            // For example, a PlayPauseButton that doesn't have play/pause symbols.
            // But we still keep track of the state of the button.
            // Only the first state item is assumed to be visible by default, i === 0,
            // for example, by default a PlayPauseButton has the PlaySymbol visible and PauseSymbol not visible.
            this._elements[i] = e ? new Sys.UI.Silverlight._DomElement(e, i === 0) : null;
        }
    }
}
Sys.UI.Silverlight._Button.prototype = {
    _down: false,
    _last: 0,
    _state: 0,    
    _repeatTimeout: null,
    _repeatClickDelegate: null,
    
    set_enabled: function(value) {
        Sys.UI.Silverlight._Button.callBaseMethod(this, "set_enabled", [value]);
        this.get_element().cursor = value ? "Hand" : "Default";
    },
    
    get_state: function() {
        // <value type="Number" integer="true">The current button state.</value>
        return this._state;
    },
    set_state: function(value) {
        if (value === this.get_state()) return;
        
        // disable the current state
        var e = this._elements[this._state];
        if (e) {
            e.set_visible(false);
        }
        
        // update state
        this._state = value;

        // enable the new state
        e = this._elements[this._state];
        if (e) {
            e.set_visible(true);
        }
    },
    
   _bindAutoAnimations: function(element, name) {
        Sys.UI.Silverlight._Button.callBaseMethod(this, "_bindAutoAnimations", [element, name]);
        this.bindEvent("mouseLeftButtonDown", name + "_MouseDown", this._mouseDown);
        this.bindEvent("mouseLeftButtonUp", name + "_MouseUp", this._mouseUp);
        this.bindEvent("mouseLeave", name + "_MouseUp", this._mouseLeave);
    },
    
    _cancelRepeat: function() {
        window.clearTimeout(this._repeatTimeout);
        this._repeatTimeout = null;
    },

	dispose: function() {
	    this._cancelRepeat();
        // dispose of child states
        if (this._elements) {
            for (var i = 0, l = this._elements.length; i < l; i++) {
                var e = this._elements[i];
                if (e) {
                    e.dispose();
                }
            }
            this._elements = null;
        }
	    
        Sys.UI.Silverlight._Button.callBaseMethod(this, 'dispose');                
    },

    _doClick: function(isDouble) {
        if (isDouble && this._doubleClickDelegate) {
            this._doubleClickDelegate(this);
        }
        else if (this._clickDelegate) {
            this._clickDelegate(this);
        }
    },

    _mouseDown: function() {
        this._down = true;
        if (this._repeatInterval && !this._repeatTimeout) {
            // click immediately when it will be repeating,
            // otherwise we don't until leftbuttonup
            this._doClick(false);
            this._repeatClickDelegate = Function.createDelegate(this, this._repeatClick);
            // the first interval is always half a second. This is so when you hold down the button it doesn't
            // immediately begin repeating. Like a keyboard, there is a delay before repeating begins.
            // when the interval is re-created this._repeatInterval is used.
            this._repeatTimeout = window.setTimeout(this._repeatClickDelegate, 500);
        }
        return true;
    },
    
    _mouseLeave: function() {
        if (!this._down) {
            // we're not already down, so don't let mouseup animation play
            return false;
        }
        // we were down and the user moved the mouse outside the button, cancel down w/o raising click
        this._down = false;
        this._cancelRepeat();
        // and allow the mouseup animation to play
        return true;
    },
    
    _mouseUp: function() {
        if (!this._down) return false;
        this._down = false;

        if (this._repeatTimeout) {
            // mouse is up, so no more repeating click callbacks
            this._cancelRepeat();
        }
        else {
            // there was no repeating. MouseUp is the click event.
            // but detect a double click...
            var last = this._last;
            this._last = new Date();
            var doubleClick = last && ((this._last - last) < 300);
            if (doubleClick) {
                // so a 3rd click won't result in another double click
                this._last = 0;
            }
            this._doClick(doubleClick);
        }
        return true;
    },

    _repeatClick: function() {
        this._repeatTimeout = window.setTimeout(this._repeatClickDelegate, this._repeatInterval);
        this._doClick(false);
    }
}
Sys.UI.Silverlight._Button.registerClass('Sys.UI.Silverlight._Button', Sys.UI.Silverlight._DomElement);

Sys.UI.Silverlight._Slider = function(element, thumbElementName, visible, changedCallback, callbackOwner) {
    this._horizontal = (element.width >= element.height);
    
    var thumbElement = element.findName(thumbElementName);
    if (!thumbElement) {
        throw Error.invalidOperation(String.format(Sys.UI.Silverlight.MediaRes.noThumbElement, element.Name, thumbElementName));
    }
    Sys.UI.Silverlight._Slider.initializeBase(this, [element, visible]);

    this._changedHandler = callbackOwner ? Function.createDelegate(callbackOwner, changedCallback) : null;

    element.cursor = "Hand";
    thumbElement.cursor = "Hand";
    this._thumb = new Sys.UI.Silverlight._DomElement(thumbElement, true);
    this._thumb.bindEvent("mouseLeftButtonDown", null, this._thumbDown, this);
    this._thumb.bindEvent("mouseLeftButtonUp", null, this._thumbUp, this);
    this._thumb.bindEvent("mouseMove", null, this._thumbMove, this);
    this.bindEvent("mouseLeftButtonDown", null, this._sliderDown);
    
    // get to the root canvas and listen to its mouseLeave, so that we can ensure we know when
    // we loose mouse capture when the mouse leaves the silverlight host while dragging
    var root = element.getHost().content.root;
    this._rootToken = root.addEventListener("mouseLeave", Function.createDelegate(this, this._thumbUp));
}
Sys.UI.Silverlight._Slider.prototype = {
    _readOnly: false,
    _dragging: false,
    _last: null,

    set_enabled: function(value) {
        if (this.get_enabled() !== value) {
            Sys.UI.Silverlight._Slider.callBaseMethod(this, "set_enabled", [value]);
            // slider's position should be 0 when disabled
            if (!value) {
                this.set_value(0);
            }
            // slider should not have hand cursor when disabled
            this.get_element().cursor = value ? "Hand" : "Default";
            // highlight bar should not be visible when disabled
            if (this._highlight) {
                this._highlight.set_visible(value);
            }
            // thumb should be invisible when disabled
            this._thumb.set_visible(value);
            this._thumb.get_element().cursor = value ? "Hand" : "Default";
        }        
    },
    
    get_readOnly: function() {
        /// <summary>Toggles whether the user can change the value interactively.</summary>
        return this._readOnly;
    },
    set_readOnly: function(value) {
        if (value !== this._readOnly) {
            this._readOnly = value;
            this._stopDragging();
        }
    },

    get_value: function() {
        // <value type="Number">A number between 0 and 1 representing the current value of the slider.</value>
        var val;
        var thumb = this._thumb.get_element();
        var slider = this.get_element();
        if (this._horizontal) {
            val = (thumb["Canvas.Left"] - slider["Canvas.Left"]) / (slider.width - thumb.width);
        }
        else {
            val = thumb["Canvas.Top"] - slider["Canvas.Top"];
            val = 1 - (val / (slider.height - thumb.height));
        }
        // make value accurate to 0.001 and cap it to within 0 and 1
        val = Math.round(val*1000)/1000;
        return Math.min(1, Math.max(0, val));
    },
    set_value: function(value) {
        this._last = null;
        if (!this._dragging) {
            // sometimes videos are longer than their NaturalDuration states.
            // NaturalDuration dictates the movement of this slider, so it would cause an attempt
            // to set the slider beyond value 1. The best way to handle this is to just cap it off.
            value = Math.max(0, Math.min(1, value));
            // if user is dragging, it's like they immediately reject the new value, so ignore it.
            this._setThumbPosition(value);
        }
    },

    _bindAutoAnimations: function(element, name) {
        Sys.UI.Silverlight._Slider.callBaseMethod(this, "_bindAutoAnimations", [element, name]);
        // look for Slider_Highlight which fills in slider's current value
        var e = element.findName(name + "_Highlight");
        if (e) {
            e[this._horizontal ? "width" : "height"] = 0;
            this._highlight = new Sys.UI.Silverlight._DomElement(e, true);
        }
        else {
            this._highlight = null;
        }
    },
    
    _detectChanged: function(value) {
        if ((value !== this._last) && this._changedHandler) {
            // the changedHandler may do something performance intensive.
            // only call it if the value has actually changed.
            this._last = value;
            this._changedHandler(this);
        }
    },

    dispose: function() {
        if (this._thumb) {
            this._thumb.dispose();
            this._thumb = null;
        }
        if (this._highlight) {
            this._highlight.dispose();
            this._highlight = null;
        }
        // rootToken might be 0 (zero) so "if (this._rootToken) ..." would result in memory leak
        if (this._rootToken !== null) {
            this.get_element().getHost().content.root.removeEventListener("mouseLeave", this._rootToken);
            this._rootToken = null;
        }
        Sys.UI.Silverlight._Slider.callBaseMethod(this, 'dispose');                
    },    
    
    _setThumbPosition: function(value) {
        var loc = this._toLocation(value);
        // loc is the position the thumb should be centered over, relative to the left/top edge of the slider
        var thumb = this._thumb.get_element();
        var slider = this.get_element();
        var h = this._highlight ? this._highlight.get_element() : null;
        if (this._horizontal) {
            // adjust to the right by the slider's left position, then adjust to the left by half the thumb
            // width in order to center it on that position
            thumb["Canvas.Left"] = loc + slider["Canvas.Left"] - (thumb.width / 2);
            if (h) {
                // highlight fills the slider up to the centered point
                h.width = loc;
            }
        }
        else {
            // adjust downward by the slider's top position, then adjust upward by half the thumb
            // width in order to center it on that position
            thumb["Canvas.Top"] = loc + slider["Canvas.Top"] - (thumb.height / 2);
            if (h) {
                // highlight fills the slider up to the centered point
                h["Canvas.Top"] = slider["Canvas.Top"] + loc;
                // adjust hieght such that higlight reaches bottom of slider
                h.height = slider.height - loc + (thumb.height / 2);
            }
        }
    },
    
    _sliderDown: function(sender, eventArgs) {
        if (this._readOnly) return false;
        
        var newValue = this._toValue(eventArgs.getPosition(sender));
        this._setThumbPosition(newValue);
        this._detectChanged(newValue);
        // begin dragging the thumb
        this._startDragging();
        return true;
    },
    
    _startDragging: function() {
        this._dragging = true;
        this._thumb.get_element().CaptureMouse();
    },
    
    _stopDragging: function() {
        if (this._dragging) {
            this._thumb.get_element().ReleaseMouseCapture();
            this._dragging = false;
        }
    },
    
    _thumbDown: function() {
        if (this._readOnly) return false;
        this._startDragging();
        return true;
    },

    _thumbUp: function() {
        if (this._readOnly) return false;
        
        if (this._dragging) {
            // was dragging. User has let go, call the changed handler.
            this._detectChanged(this.get_value());
        }
        // stop dragging
        this._stopDragging();
        return true;
    },

    _thumbMove: function(sender, args) {
        if (this._dragging) {
            this._setThumbPosition(this._toValue(args.getPosition(this.get_element())));
        }
        return true;
    },
    
    _toLocation: function(value) {
        // converts a logical value into a point which is relative to the Slider control's position
        value = Math.min(1, Math.max(0, value));
        
        var thumb = this._thumb.get_element();
        var slider = this.get_element();
        var range;
        
        if (this._horizontal) {
            range = slider.width - thumb.width;
            // the percentage along the available width, offset to the right by half the thumb width
            return (thumb.width / 2) + (value * range);
        }
        else {
            // vertical sliders move positively in the negative Y direction (UP)
            // so adjust value with 1-value
            range = slider.height - thumb.height;
            return (thumb.height / 2) + ((1-value) * range);
        }
    },
    
    _toValue: function(point) {
        // converts a point which is relative to the Slider to a logical value
        var val;
        var thumb = this._thumb.get_element();
        var slider = this.get_element();
        if (this._horizontal) {
            // determine the percentage X is into the available range.
            // available range is sliderWidth-thumbWidth. Range doesn't begin
            // until thumbWidth/2 so subtract that off first.
            val = (point.X - (thumb.width / 2)) / (slider.width - thumb.width);
        }
        else {
            val = (point.Y - (thumb.height / 2)) / (slider.height - thumb.height);
            val = 1 - val;
        }
        // make it accurate to 0.001 and cap it between 0 and 1
        // its possible to get below 0 or above 1 because the range of motion of the thumb
        // is limited by its width within the width of the slider, and the user could click
        // on a region beyond which the thumb can travel.
        val = Math.round(val*1000)/1000;
        return Math.min(1, Math.max(0, val));
    }    
}
Sys.UI.Silverlight._Slider.registerClass('Sys.UI.Silverlight._Slider', Sys.UI.Silverlight._DomElement);

Sys.UI.Silverlight._TextBlock = function(element, bgElement, visible) {
    Sys.UI.Silverlight._TextBlock.initializeBase(this, [element, visible]);
    if (bgElement) {
        this._bg = new Sys.UI.Silverlight._DomElement(bgElement, visible);
        // background element is given.
        // text will be centered by moving this background to center on its original
        // center position. It will also be set to the width/height to just fit the text.
        // this gives text a colored background color as well as centers it relative to
        // its authored position.
        this._centerX = bgElement["Canvas.Left"] + bgElement.width / 2;
        this._bottomY = bgElement["Canvas.Top"] + bgElement.height;
    }
    else {
        this._bg = null;
        // no background element, so we will center the textblock based on its current position
        // and size. This allows authors to fill textblocks with default text that represents the
        // largest possible size within which the text is centered. For example, media time index
        // will read "00:00:00" at author-time. When the text is set to "1:24" it will be centered
        // with regard to how wide the original authored text was.
        this._centerX = element["Canvas.Left"] + element.ActualWidth / 2;
        this._bottomY = element["Canvas.Top"] + element.ActualHeight;
    }
}
Sys.UI.Silverlight._TextBlock.prototype = {
    get_text: function() {
        return this.get_element().Text || "";
    },
    set_text: function(value) {
        var element = this.get_element();
        element.Text = value || "";
        // when text set to nothing, make textblock invisible
        this.set_visible(!!value);
        // apply changes to the background element if it exists, or the textblock itself if not
        var bg = this._bg ? this._bg.get_element() : element;
        // move the background element such that the text will be centered with respect to its
        // original location. And modify the BG width and height to just fit the text.
        bg.width = element.ActualWidth;
        bg.height = element.ActualHeight;
        bg["Canvas.Left"] = this._centerX - bg.width / 2;
        bg["Canvas.Top"] = this._bottomY - bg.height;
    },
    
    set_visible: function(value) {
        Sys.UI.Silverlight._TextBlock.callBaseMethod(this, "set_visible", [value]);
        if (this._bg) {
            this._bg.set_visible(value);
        }
    },    

    dispose: function() {
        Sys.UI.Silverlight._TextBlock.callBaseMethod(this, "dispose");
        if (this._bg) {
            this._bg.dispose();
        }
    }
}
Sys.UI.Silverlight._TextBlock.registerClass('Sys.UI.Silverlight._TextBlock', Sys.UI.Silverlight._DomElement);

Sys.UI.Silverlight._ProgressBar = function(element, textElement, visible) {
    Sys.UI.Silverlight._ProgressBar.initializeBase(this, [element, visible]);
    
    this._fullWidth = element.width;
    element.width = 0;
    
    if (textElement) {
        this._text = new Sys.UI.Silverlight._TextBlock(textElement, null, visible);
        this._text.set_text("");
    }
    else {
        this._text = null;
    }
}
Sys.UI.Silverlight._ProgressBar.prototype = {
    get_value: function() {
        var val = this._fullWidth !== 0 ? (this.get_element().width / this._fullWidth) : 0;
        // make accurate to 0.001
        return Math.round(val * 1000) / 1000;
    },
    set_value: function(value) {
        this.get_element().width = this._fullWidth * value;
        if (this._text) {
            // set percentage to the text block
            this._text.set_text("" + Math.floor(value * 100));
        }
    },
    
    set_visible: function(value) {
        Sys.UI.Silverlight._ProgressBar.callBaseMethod(this, "set_visible", [value]);
        if (this._text) {
            this._text.set_visible(value);
        }
    },
   
    dispose: function() {
        Sys.UI.Silverlight._ProgressBar.callBaseMethod(this, "dispose");
        if (this._text) {
            this._text.dispose();
        }
    }
}
Sys.UI.Silverlight._ProgressBar.registerClass('Sys.UI.Silverlight._ProgressBar', Sys.UI.Silverlight._DomElement);

Sys.UI.Silverlight._ImageList = function(element, toggleElement, visible, itemClickCallback, callbackOwner) {
    // a scrollable list of images.
    // This is one of the more complex of the _DomElement classes.
    // Some important notes on understanding how it works:
    
    // STRUCTURE OF THE XAML
    // the images to scroll over can be dynamic while the silverlight images used
    // to display them is static. The xaml author creates physical items to display
    // each image, plus one extra item that is just out of view. As the user scrolls
    // left or right, the extra item (the "overflow" item) is repositioned so that it
    // scrolls into view. A previously visible item scrolls out of view and becomes the
    // new offset item. As the physical items are repositioned, their assigned image is 
    // changed to be equal to their corresponding virtual item. This gives the illusion
    // that there are N number of images. It also prevents downloading of the images until
    // they are visible, and only uses the resources needed for the visible items.
    // example:
    // /---------------------------------\
    // |__[ITEM1]____[ITEM2]____[ITEM3]__|__[ITEM4] <-- overflow item
    // \---------------------------------/
    // When the image list scrolls left, ITEM4 becomes the 3rd visible item and ITEM1 becomes
    // the new overflow item. If it scrolled right instead, ITEM4 would be moved to the position
    // left of ITEM1 before the scrolling animation started.
    
    // ITEMS THAT DON'T HAVE IMAGES
    // Note that this also supports the ability for the bound items to only sparsely contain
    // actual image urls. For example, there may be 10 chapters assigned but only chapters
    // 1, 5, and 8 have image thumbnails. The MediaPlayer supports programmatic access to the
    // chapters (such as the ability to set the current chapter to #2, and for chapterStarted
    // events to fire for the imageless chapters), but this control only actually displays the
    // chapters that have thumbnails.
    
    // TURNING THE IMAGE LIST ON AND OFF
    // MediaPlayer supports a button that allows the user to toggle display of the chapter listing
    // on and off. The given toggleElement will act as the button to do that. When the button is
    // clicked, it toggles the visibility of the chapter list element.
    // MediaPlayer also supports chapter lists that automatically become visible when the mouse
    // is over them. This allows the user to just point at the area where chapters are displayed and
    // it fades into view. There's no special code handling for that here -- the base DomElement
    // class would automatically hookup the ChapterArea_MouseEnter event. There simply will be no
    // toggleElement, which is why that element is optional.
    
    // DISABLING THE CHAPTER LIST
    // Most videos will not have chapters. In order to not display an empty chapter list UI, the
    // chapter list has the ability to be "deativiated". When deactivated, the element and the toggle
    // element are hidden, and if it is a MouseOver style chapter list, it will not respond to mouse
    // over events.
    
    // So there are two related but separate concepts:
    // (1) Visibility: whether the chapter list is currently displayed, controlled through mouseOver
    //                 or the toggle element.
    // (2) Activated:  whether the chapter list CAN be displayed. The chapter list may be active
    //                 but not visible. When deactivated, even the toggle element is hidden.
    // Visibility is controlled through user interaction. 
    // Activation is controlled through the get/set_active property.
	this._horizontal = (element && (element.width >= element.height));
	this._reference = (this._horizontal ? "Canvas.Left" : "Canvas.Top");
    Sys.UI.Silverlight._ImageList.initializeBase(this, [element, visible]);
    
    this._toggle = toggleElement ?
        new Sys.UI.Silverlight._Button(toggleElement, visible, 0, this._onToggle, null, this) :
        null;

    this._itemClickDelegate = Function.createDelegate(callbackOwner, itemClickCallback);
    // virtual items is the dynamic list bound to
	this._virtualItems = [];
	// contains the indexes of the virtual items that have images
	// this is used to map a clicked image to its virtual item
	this._imageItems = [];
}
Sys.UI.Silverlight._ImageList.prototype = {
    _next: null,
    _previous: null,
    _scrollAnimation: null,
    _scrollStoryboard: null,
    _itemSize: 0,
    _itemSpacing: 0,
    _canActivate: true,
    _active: false,
    // scrollOffset stores the scrolling offset, indicating the image index of the
    // left most visible item.
    _scrollOffset: 0,
    // overflowIndex keeps track of the physical item that is currently off the edge of the
    // visible area. By default it is the last item in the list, but after scrolling that
    // item comes into view. The new overflow item depends on the direction scrolled.
    _overflowIndex: 0,
    
    get_active: function() {
        return this._active;
    },
    set_active: function(value) {
        if (value !== this.get_active()) {
            this._active = value;
            // element is activated but not visible through this action, because
            // chapter area have opacity=0 until made visible with the toggle element
            // or the mouseenter event.
            this.get_element().visibility = value ? 0 : 1;
            if (this._toggle) {
                // toggle button should be visible when activated, collapsed when deactivated
                this._toggle.set_visible(value);
            }
            if (value) {
                if (!this._toggle) {
                    // if the chapter list has no toggle, then we assume its made visible through
                    // the MouseEnter event, in which case its important to make it hit testable
                    // for that to work. If it does have a toggle button, the button will make
                    // the image list visible, and it should remain not HitTestable so that it doesn't
                    // occlude part of the canvas while it isn't toggled on.
                    this.get_element().IsHitTestVisible = true;
                }
            }
            else {
                // deactivating the chapter area automatically hides it
                // otherwise we could have a deactivated chapter area that still hit testable
                // NOTE: this also makes the element not HitTestable, which is correct whether
                // there's a toggle button or not.
                this.set_visible(false);
            }
        }
    },
    
    get_canActivate: function() {
        return this._canActivate;
    },
    set_canActivate: function(value) {
        if (value !== this.get_canActivate()) {
            this._canActivate = value;
            this._ensureActivation();
        }
    },
    
    get_items: function() {
        return this._virtualItems;
    },
    set_items: function(value) {
        this._virtualItems = value || [];
        this._imageItems = [];
        if (value) {
            for (var i = 0, l = value.length; i < l; i++) {
                if (value[i].get_thumbnailSource()) {
                    // item has an image.
                    this._imageItems[this._imageItems.length] = i;
                }
            }
        }
        this._ensureActivation();
    },
    
    set_visible: function(value) {
        Sys.UI.Silverlight._ImageList.callBaseMethod(this, "set_visible", [value]);
        // make sure the chapter area isn't hit testable when its supposed to be hidden
        // For example, a chapter area that normally appears on top of the video should not prevent clicks
        // from going through it when it is toggled off.
        this.get_element().IsHitTestVisible = value;
    },
    
    _assignImages: function() {
        // assigns each physical item the image they are assigned to display based
        // on their corresponding virtual item index.
        // If there are less image items than physical items, the remaining
        // items are hidden.
        for (var i = 0, l = this._items.length; i < l; i++) {
            var item = this._items[i];
            var offset = this._scrollOffset + i;
            if (offset < this._imageItems.length) {
                var e = item.image.get_element();
                var vitem = this._virtualItems[this._imageItems[offset]];
                // Clear the source first, so if the image fails to load
                // it won't display an incorrect image.
                e.source = null;
                e.source = vitem.get_thumbnailSource();
                item.button.set_visible(true);
                item.button._imageIndex = offset;
                if (item.title) {
                    item.title.set_text(vitem.get_title());
                }
            }
            else {
                item.button.set_visible(false);
                item.button._imageIndex = null;
            }
        }
    },

    _bindAutoAnimations: function(element, name) {
        Sys.UI.Silverlight._ImageList.callBaseMethod(this, "_bindAutoAnimations", [element, name]);
        // the storyboard that plays the scrolling animation
        var sb = element.findName(name + "_ScrollAnimationStoryboard");
        var anim = element.findName(name + "_ScrollAnimation");
        if (sb && anim) {
            this._scrollStoryboard = sb;
            this._scrollAnimation = anim;
            // fire the animation via autorepeat buttons
            // repeat the animation when it ends by using its duration
            var interval = anim.duration.seconds * 1000;
            var next = element.findName(name + "_ScrollNext");
            var prev = element.findName(name + "_ScrollPrevious");
            if (next && prev) {
                this._next = new Sys.UI.Silverlight._Button(next, true, interval, this._scrollNext, null, this);
                this._previous = new Sys.UI.Silverlight._Button(prev, true, interval, this._scrollPrevious, null, this);
            }
        }
        this._bindItems(element, name);
    },
    
    _bindItems: function(e, name) {
        // Find the physical items authored in the xaml with a naming scheme.
        // Each item has a canvas and an image with name like List_ScrollItem1 and List_ScrollItem1_Image
        // _items contains the list of physical items found in the xaml
        this._items = [];
        var item, image, text;
        for (var i = 1;
                item = e.findName(name + "_ScrollItem" + i),
                image = e.findName(name + "_ScrollItem" + i + "_Image"),
                title = e.findName(name + "_ScrollItem" + i + "_Title"),
                item && image; i++) {
            this._items[i-1] = {
                    button: new Sys.UI.Silverlight._Button(item, true, 0, this._itemClick, null, this),
                    image: new Sys.UI.Silverlight._DomElement(image, true),
                    title: (title ? new Sys.UI.Silverlight._TextBlock(title, null, true) : null)
                };
        }
        
        if (this._items.length > 0) {
            // the distance the first item is from the left of the container lets us know how to space
            // out each physical item when they are scrolled left and right. The distance is half
            // what the spacing is between each item. This centers the items within the container
            // while giving the xaml author the ability to control how far apart the items will be.
            // For example:
            // |__[ITEM1]____[ITEM2]____[ITEM3]__|
            var first = this._items[0].button.get_element();
            this._itemSize = this._horizontal ? first.width : first.height;
            this._itemSpacing = first[this._reference] * 2;
        }
    },
    
    dispose: function() {
        if (this._next) {
            this._next.dispose();
        }
        if (this._previous) {
            this._previous.dispose();
        }
        if (this._toggle) {
            this._toggle.dispose();
        }
        for (var i = 0, l = this._items.length; i < l; i++) {
            var item = this._items[i];
            item.button.dispose();
            item.image.dispose();
            if (item.title) {
                item.title.dispose();
            }
        }
        this._virtualItems = null;
        this._imageItems = null;
        this._scrollAnimation = null;
        this._scrollStoryboard = null;
        Sys.UI.Silverlight._ImageList.callBaseMethod(this, "dispose");
    },
    
    _ensureActivation: function() {
        if (this._imageItems.length === 0 || !this.get_canActivate()) {
            // there are no items or none that have images,
            // OR there are images but the media is not seekable (canActivate=false)
            // automatically deactivate
            this.set_active(false);
        }
        else {
            this.set_active(true);
            this._reset();
            this._assignImages();
        }
    },

    _handleOverflow: function(direction) {
        // ensures the overflow item is in the right position
        
        // determine virtual index of the overflow item
        var layoutIndex = direction === 1 ? (this._items.length-1) : -1;
        var imageIndex = this._scrollOffset + layoutIndex;
        
        // assign virtual item data to the overflow item which is about to come into view
        var item = this._items[this._overflowIndex];
        var e = item.image.get_element();
        var vitem = this._virtualItems[this._imageItems[imageIndex]];
        // Clear the source first, so if the image fails to load
        // it won't display an incorrect image.
        e.source = null;
        e.source = vitem.get_thumbnailSource();
        item.button._imageIndex = imageIndex;
        if (item.title) {
            item.title.set_text(vitem.get_title());
        }
        
        // position the overflow item to the correct location so it can scroll into view
        var button = item.button.get_element();
        button[this._reference] = imageIndex * (this._itemSize + this._itemSpacing) + (this._itemSpacing / 2);
        
        // overflow item is no longer an overflow item, advance the overflowIndex
        this._overflowIndex += direction;
        if (this._overflowIndex < 0) {
            this._overflowIndex = this._items.length - 1;
        }
        else if (this._overflowIndex >= this._items.length) {
            this._overflowIndex = 0;
        }
    },
    
    _itemClick: function(button) {
        var index = button._imageIndex;
        if (index !== null) {
            // pass the virtual item index that was clicked
            this._itemClickDelegate(this._imageItems[index]);
        }
    },
    
    _onToggle: function() {
        this.set_visible(!this.get_visible());
    },
    
    _reset: function() {
	    // reset scroll offset to 0 and reset the physical items to line up in their natural order again
	    // this is done when changing chapter lists dynamically.
	    var oldOffset = this._scrollOffset;
	    this._scrollOffset = 0;
	    for (var i = 0, l = this._items.length; i < l; i++) {
	        var button = this._items[i].button;
	        button._imageIndex = i;
    	    button.get_element()[this._reference] = i * (this._itemSize + this._itemSpacing) + (this._itemSpacing/2);
	    }
	    this._overflowIndex = this._items.length - 1;
	    // ensure scrolled to the beginning
        if (this._scrollAnimation && (oldOffset !== 0)) {
            this._scrollAnimation.To = "0";
            this._scrollStoryboard.begin();
        }
    },
    
    _scroll: function(direction) {
        if (this._scrollAnimation) {
            // ensure the overflow item is in the right location and the new overflow item is known
            this._handleOverflow(direction);

            var fromOffset = this._scrollOffset;
            this._scrollOffset += direction;

            // scroll the image list from the old offset to the new offset
            this._scrollAnimation.From = "-" + (fromOffset * (this._itemSize + this._itemSpacing));
            this._scrollAnimation.To = "-" + (this._scrollOffset * (this._itemSize + this._itemSpacing));
            this._scrollStoryboard.begin();
        }
        else {
            // when no scroll storyboard exists, just reassign the images with no animation
            this._scrollOffset += direction;
            this._assignImages();            
        }
    },
    
    _scrollNext: function() {
        if (this._scrollOffset < (this._imageItems.length - this._items.length + 1)) {
            this._scroll(1);
        }
    },
    
    _scrollPrevious: function() {
        if (this._scrollOffset > 0) {
            this._scroll(-1);
        }
    }
}
Sys.UI.Silverlight._ImageList.registerClass('Sys.UI.Silverlight._ImageList', Sys.UI.Silverlight._DomElement);

Sys.UI.Silverlight.MarkerEventArgs = function(marker) {
    /// <param name="marker">The Silverlight marker.</param>
    this._marker = marker;
    Sys.UI.Silverlight.MarkerEventArgs.initializeBase(this);
}
Sys.UI.Silverlight.MarkerEventArgs.prototype = {
    get_marker: function() {
        /// <value>The Silverlight marker.</value>
        return this._marker || null;
    }
}
Sys.UI.Silverlight.MarkerEventArgs.registerClass("Sys.UI.Silverlight.MarkerEventArgs", Sys.EventArgs);

Sys.UI.Silverlight.MediaChapterEventArgs = function(chapter) {
    /// <param name="chapter" mayBeNull="true" type="Sys.UI.Silverlight.MediaChapter">The media chapter.</param>
    this._chapter = chapter;
    Sys.UI.Silverlight.MediaChapterEventArgs.initializeBase(this);
}
Sys.UI.Silverlight.MediaChapterEventArgs.prototype = {
    get_chapter: function() {
        /// <value mayBeNull="true" type="Sys.UI.Silverlight.MediaChapter">The media chapter.</value>
        return this._chapter || null;
    }
}
Sys.UI.Silverlight.MediaChapterEventArgs.registerClass("Sys.UI.Silverlight.MediaChapterEventArgs", Sys.CancelEventArgs);

Sys.UI.Silverlight.MediaChapter = function(title, position, thumbnailSource) {
    /// <summary>Defines a position in a media source.</summary>
    /// <param name="title" type="String" mayBeNull="true">The title of the chapter.</param>
    /// <param name="position" type="Number">The position of the chapter in seconds.</param>
    /// <param name="thumbnailSource" type="String" mayBeNull="true">The source of a thumbnail.</param>
    if (position < 0) {
        throw Error.argumentOutOfRange("position", position);
    }
    this._title = title;
    this._position = position;
    this._thumbnailSource = thumbnailSource;
    Sys.UI.Silverlight.MediaChapter.initializeBase(this);
}
Sys.UI.Silverlight.MediaChapter.prototype = {
    get_position: function() {
        /// <value type="Number">The position of the chapter in seconds.</value>
        return this._position;
    },
    get_thumbnailSource: function() {
        /// <value type="String">The source of a thumbnail.</value>
        return this._thumbnailSource || "";
    },
    get_title: function() {
        /// <value type="String">The chapter title.</value>
        return this._title || "";
    }
}
Sys.UI.Silverlight.MediaChapter._createChapters = function() {
    // param array: "title1", position1, "thumbnailSource1, "title2", position2, "thumbnailSource2", ...
    var list = [];
    for (var i = 0, l = arguments.length; i < l; i += 3) {
        if (i+2 >= arguments.length) {
            throw Error.argument("arguments");
        }
        var title = arguments[i] || "";
        var position = arguments[i+1];
        var source = arguments[i+2] || "";
        if (typeof(title) !== "string") {
            throw Error.argumentType("title", typeof(title), String);
        }
        if (typeof(position) !== "number") {
            throw Error.argumentType("position", typeof(position), Number);
        }
        if (typeof(source) !== "string") {
            throw Error.argumentType("thumbnailSource", typeof(source), String);
        }
        list[list.length] = new Sys.UI.Silverlight.MediaChapter(arguments[i], arguments[i+1], arguments[i+2]);
    }
    return list;
}
Sys.UI.Silverlight.MediaChapter.registerClass("Sys.UI.Silverlight.MediaChapter");

Sys.UI.Silverlight.MediaPlayer = function(domElement) {
    /// <summary>A Silverlight media player.</summary>
    /// <param name="domElement" domElement="true">Silverlight host element.</param>
    this._children = {};
    this._timeline = [];
    Sys.UI.Silverlight.MediaPlayer.initializeBase(this, [domElement]);
}
Sys.UI.Silverlight.MediaPlayer.prototype = {
    _autoPlay: false,
    _autoLoad: true,
    _forcePlay: false,
    _bufferPlaying: false,
    _canSeek: false,
    _caption: "",
    _chapters: null,
    _chapterStarted: -1,
    _duration: 0,
    _enableCaptions: true,
    _me: null,
    _mediaAvailable: false,
    _mediaSource: "",
    _muted: false,
    _oldState: null,
    _placeholder: "",
    _toggledCaptions: true,
    _volume: 0.5,
    _forcePlayOnStop: false,

    add_chapterSelected: function(handler) {
        this.get_events().addHandler("chapterSelected", handler);
    },
    remove_chapterSelected: function(handler) {
        this.get_events().removeHandler("chapterSelected", handler);
    },
    
    add_chapterStarted: function(handler) {
        this.get_events().addHandler("chapterStarted", handler);
    },
    remove_chapterStarted: function(handler) {
        this.get_events().removeHandler("chapterStarted", handler);
    },
    
    add_currentStateChanged: function(handler) {
        this.get_events().addHandler("currentStateChanged", handler);
    },
    remove_currentStateChanged: function(handler) {
        this.get_events().removeHandler("currentStateChanged", handler);
    },

    add_markerReached: function(handler) {
        this.get_events().addHandler("markerReached", handler);
    },
    remove_markerReached: function(handler) {
        this.get_events().removeHandler("markerReached", handler);
    },

    add_mediaEnded: function(handler) {
        this.get_events().addHandler("mediaEnded", handler);
    },
    remove_mediaEnded: function(handler) {
        this.get_events().removeHandler("mediaEnded", handler);
    },

    add_mediaFailed: function(handler) {
        this.get_events().addHandler("mediaFailed", handler);
    },
    remove_mediaFailed: function(handler) {
        this.get_events().removeHandler("mediaFailed", handler);
    },

    add_mediaOpened: function(handler) {
        this.get_events().addHandler("mediaOpened", handler);
    },
    remove_mediaOpened: function(handler) {
        this.get_events().removeHandler("mediaOpened", handler);
    },

    add_volumeChanged: function(handler) {
        this.get_events().addHandler("volumeChanged", handler);
    },
    remove_volumeChanged: function(handler) {
        this.get_events().removeHandler("volumeChanged", handler);
    },

    get_autoPlay: function() {
        /// <value type="Boolean">Toggles whether the media automatically plays when opened.</value>
        return this._autoPlay;
    },
    set_autoPlay: function(value) {
        this._autoPlay = value;
        if (this._me) {
            this._me.autoPlay = value;
            this._ensureMedia();
        }
    },

    get_autoLoad: function() {
        /// <value type="Boolean">Toggles whether the media automatically loads.</value>
        return this._autoLoad;
    },
    set_autoLoad: function(value) {
        this._autoLoad = value;
        if (this._me) {
            this._ensureMedia();
        }
    },

    get_caption: function() {
        /// <value type="String">Gets or sets the current caption.</value>
        return this._caption;
    },
    set_caption: function(value) {
        this._caption = value;
        this._ensureCaption();
    },
    
    get_chapters: function() {
        /// <value type="Array" mayBeNull="true" elementMayBeNull="false" elementType="Sys.UI.Silverlight.MediaChapter">
        ///     An array of chapter points.
        /// </value>
        if (this._chapters) {
            return Array.clone(this._chapters);
        }
        return [];
    },
    set_chapters: function(value) {
        this._chapters = value;
        this._setProperties("items", ["ChapterArea"], value);
        this._timeline = [];
        if (value) {
            // timeline stores the start time of each chapter, for quick and easy
            // current chapter lookup
            for (var i = 0, l = value.length; i < l; i++) {
                this._timeline[this._timeline.length] = value[i].get_position();
            }
        }
        this._ensureChapterStarted(true);
    },
    
    get_currentChapter: function() {
        /// <summary>Gets or sets the current media chapter.</summary>
        /// <value type="Sys.UI.Silverlight.MediaChapter" mayBeNull="true"></value>
        return this._chapterStarted === -1 ? null : this.get_chapters()[this._chapterStarted];
    },
    set_currentChapter: function(value) {
        this._ensureLoaded();
        var chapters = this.get_chapters();
        // cannot set to a null chapter, even though get_currentchapter may be null
        if (!value) {
            throw Error.argumentNull("value");
        }
        // ensure the given chapter is one of the assigned chapters and not some orphan chapter
        var found = false;
        for (var i = 0, l = chapters.length; i < l; i++) {
            if (chapters[i] === value) {
                found = true;
                break;
            }
        }
        if (!found) {
            throw Error.argument("value", Sys.UI.Silverlight.MediaRes.invalidChapter);
        }
        this.set_position(value.get_position());
        this._ensureChapterStarted(false);
    },
    
    get_currentState: function() {
        /// <value type="String" mayBeNull="true">Gets the current state of the media. May be null if the state is unknown.</value>
        return this._me ? this._me.currentState : null;
    },

    get_enableCaptions: function() {
        /// <value type="Boolean">Specifies whether captions in the media are displayed.</value>
        return this._enableCaptions;
    },
    set_enableCaptions: function(value) {
        if (value !== this.get_enableCaptions()) {
            this._enableCaptions = value;
            this._ensureCaption();
        }
    },
    
    get_mediaElement: function() {
        /// <value>Gets the Silverlight MediaElement associated with the Media Player.</value>
        return this._me;
    },

    get_mediaSource: function() {
        /// <value type="String">Gets or sets the current media source.</value>
        return this._mediaSource;
    },
    set_mediaSource: function(value) {    
        this._mediaSource = value;
        this._forcePlay = false;
        if (this._me) {
            this._loadPlaceholder();
            var queue = !this.get_autoPlay() && !this.get_autoLoad();
            this._me.source = queue ? null : value;
            // its possible with !AutoLoad that we should now activate or deactivate the StartButton,
            // since a media source is now either available or not available.
            if (queue) {
                this._ensureMedia();
            }
        }
    },

    get_muted: function() {
        /// <value type="Boolean">Gets or sets whether the media audio is muted.</value>        
        return this._muted;
    },
    set_muted: function(value) {
        if (value !== this.get_muted()) {
            this._muted = value;
            if (this._me) {
                this._me.isMuted = value;
                // set the proper state on the mute button
                this._setProperties("state", ["MuteButton"], value ? 1 : 0);
            }
            this.onVolumeChanged(Sys.EventArgs.Empty);
            this._raiseEvent("volumeChanged");
        }
    },

    get_placeholderSource: function() {
        /// <value type="String">Source for placeholder to display before media is opened.</value>
        return this._placeholder;
    },
    set_placeholderSource: function(value) {
        this._placeholder = value;
    },
    
    get_position: function() {
        /// <value type="Number">The current media position in seconds.</value>
        return this._me ? this._me.position.seconds : 0;
    },
    set_position: function(value) {
        if (isNaN(value) || !isFinite(value)) {
            throw Error.argumentOutOfRange("value", value);
        }
        // as with Silverlight itself, attempting to seek when seeking is not allowed is simply ignored.
        this._ensureLoaded();
        if (!this._canSeek) return;
        this._mediaEnded = false;
        // caption is invalid if video position is changed
        this.set_caption("");
        
        // restrict to within 0 and naturalduration.
        value = Math.min(this._duration, Math.max(0, value));
        var position = this._me.position;
        position.seconds = value;
        this._me.position = position;            
        
        this._ensurePosition(value);
    },
    
    get_volume: function() {
        /// <value type="Number">Volume level between 0 and 1</value>        
        return this._volume;
    },
    set_volume: function(value) {
        if (value < 0 || value > 1) {
            throw Error.argumentOutOfRange("value", value, Sys.UI.Silverlight.MediaRes.volumeRange);
        }
        if (value !== this.get_volume()) {
            this._volume = value;
            // cannot set volume when media element is "Closed".
            // When meOpened event occurs we'll reset the volume.
            if (this._me && this._me.currentState !== "Closed") {
                this._me.volume = value;
            }
            this.onVolumeChanged(Sys.EventArgs.Empty);
            this._raiseEvent("volumeChanged");
        }
        
        // update the volume slider
        this._setProperties("value", ["VolumeSlider"], value);
    },
    
    _bindAllControls: function() {
        var c = this.get_element().content.root;
        this._bindElements(c,
            [   [/*DomElements*/ 0,
                    ["FullScreenVideoWindow", false],
                    ["BufferingArea", false],
                    ["PlayerControls", true],
                    ["PlaceholderImage", false]
                ],
                [/*Buttons*/ 1,
                    ["VideoWindow", true, 0, this._onTogglePlayPause, this._meDoubleClick, this],
                    ["FullScreenArea", false, 0, this._onTogglePlayPause, this._meDoubleClick, this],
                    ["PlayButton", true, 0, this._onPlay, null, this],
                    ["StartButton", false, 0, this._onPlay, null, this],
                    ["PlayPauseButton", true, 0, this._onTogglePlayPause, null, this, ["PlaySymbol", "PauseSymbol"]],
                    ["StopButton", true, 0, this._onStop, null, this],
                    ["PauseButton", true, 0, this._onPause, null, this],
                    ["MuteButton", true, 0, this._onMute, null, this, ["MuteOffSymbol", "MuteOnSymbol"]],
                    ["FullScreenButton", true, 0, this._onToggleFullScreen, null, this],
                    ["NextButton", true, 0, this._onNext, null, this],
                    ["PreviousButton", true, 0, this._onPrevious, null, this],
                    // 20 = time in milliseconds between clicks
                    // Holding down the VolumeUp and Down buttons continuously changes the volume this way.
                    // Note that while 20 = 20 milliseconds, actual clicking doesn't occur every 20
                    // milliseconds due to the overhead with modifying the volume on each tick.
                    // 20 is used because in testing on the various browsers it results in the optimal speed.
                    // Also, repeating does not begin immediately. There is a half second delay first, then
                    // repeating begins. This is so that it is possible to click on the button a single
                    // time without causing unwanted multiple clicks.
                    ["VolumeUpButton", true, 20, this._onVolumeUp, null, this],
                    ["VolumeDownButton", true, 20, this._onVolumeDown, null, this],
                    ["CaptionToggleButton", false, 0, this._onCaptionToggle, null, this, ["CaptionOnSymbol", "CaptionOffSymbol"]]
                ],
                [/*TextBlocks*/ 2,
                    ["TotalTimeText", null, true],
                    ["CurrentTimeText", null, true],
                    ["CaptionText", c.findName("CaptionArea"), false],
                    ["BufferingText", null, false],
                    ["FullScreenCaptionText", c.findName("FullScreenCaptionArea"), false]
                ],
                [/*Sliders*/ 3,
                    ["TimeSlider", "TimeThumb", true, this._onTimeChanged, this],
                    ["VolumeSlider", "VolumeThumb", true, this._onVolumeChanged, this]
                ],
                [/*ImageLists*/ 4,
                    ["ChapterArea", c.findName("ChapterToggleButton"), false, this._onChapterClick, this]
                ],
                [/*ProgressBars*/ 5,
                    ["DownloadProgressSlider", c.findName("DownloadProgressText"), true]
                ]]);

        // when buffering occurs we call begin() on this storyboard
        this._bufferingStoryboard = c.findName("BufferingArea_Buffering");
        
        var me = this._children["VideoWindow"];
        if (!me) {
            throw Error.invalidOperation(Sys.UI.Silverlight.MediaRes.noMediaElement);
        }
        this._me = me.get_element();
        me.bindEvent("mediaOpened", null, this._meOpened, this);
        me.bindEvent("mediaFailed", null, this._meFailed, this);
        me.bindEvent("mediaEnded", null, this._meEnded, this);
        me.bindEvent("downloadProgressChanged", null, this._meDownloadProgress, this);
        me.bindEvent("bufferingProgressChanged", null, this._meBufferingProgress, this);
        me.bindEvent("markerReached", null, this._meMarker, this);
        me.bindEvent("currentStateChanged", null, this._meState, this);
    },
    
    _bindElements: function(c, list) {
        // this helper reduces script size by consolidating logic used when creating all the _DomElement classes
        // required to give the well-known elements their functionality.
        for (var i = 0, l = list.length; i < l; i++) {
            var typeList = list[i];
            //typeList = [typeCode, element1, element2, element3, etc..]
            var typeCode = typeList[0];
            for (var j = 1, jl = typeList.length; j < jl; j++) {
                var e = typeList[j];
                // e = [elementName, param1, param2, param3, ...]
                var name = e[0];
                var el = c.findName(name);
                if (!el) continue;
                var type;
                switch (typeCode) {
                    case 0: type = "_DomElement"; break;
                    case 1: type = "_Button"; break;
                    case 2: type = "_TextBlock"; break;
                    case 3: type = "_Slider"; break;
                    case 4: type = "_ImageList"; break;
                    case 5: type = "_ProgressBar"; break;
                }
                type = Sys.UI.Silverlight[type];
                this._children[name] = new type(el, e[1], e[2], e[3], e[4], e[5], e[6]);
            }
        }
    },
    
    _detectChapterChange: function(position) {
        if (this._timeline.length === 0) return;
        // when did the current chapter begin (-Infinity if no current chapter)
        var startTime = (this._chapterStarted === -1) ? -Infinity : this._timeline[this._chapterStarted];
        // when does the next chapter begin (Infinity if current chapter is the last chapter)
        var endTime = (this._chapterStarted + 1 >= this._timeline.length) ? Infinity : this._timeline[this._chapterStarted + 1];
        // chapter boundary hit if current time falls outside start and end
        if ((position < startTime) || (position > endTime)) {
            // user may have naturally played through to the next chapter
            // or may have seeked into a new chapter.
            this._ensureChapterStarted(false, position);
        }
    },

    _enableBuffering: function(percent) {
        var enabled = (percent !== null) && (percent < 100);
        if (enabled) {
            this._setProperties("text", ["BufferingText"], Math.floor(percent).toString());
        }
        this._setProperties("visible", ["BufferingText", "BufferingArea"], enabled);

        var bsb = this._bufferingStoryboard;
        if (!bsb) return;

        if (!enabled) {
            // always ok to call stop
            bsb.stop();
            this._bufferPlaying = false;
        }
        else if (!this._bufferPlaying) {
            // important not to call Begin() if its already playing
            // because it would cause the animation to flutter
            bsb.begin();
            this._bufferPlaying = true;
        }
    },
    
    _ensureCaption: function() {
        // if captions shouldnt be shown set it to "" otherwise set it to the current caption
        var showCaptions = this._toggledCaptions && this.get_enableCaptions();
        var caption = showCaptions ? this.get_caption() : "";
        this._setProperties("text", ["CaptionText", "FullScreenCaptionText"], caption);
        if (caption) {
            // the button that disabled/enables captions is not visible by default.
            // the first time a caption is encountered it is turned on. This way, the caption button
            // doesn't appear at all for videos that don't have captions.
            this._setProperties("visible", ["CaptionToggleButton"], true);
        }
    },
    
    _ensureChapterStarted: function(force, position) {
        if (!this._me) return;
        if (!position) {
            position = this.get_position();
        }
        var c = this._canSeek ? this._getChapterAt(position) : -1;
        if ((force && (c !== -1 || c !== this._chapterStarted)) ||
            (c !== this._chapterStarted)) {
            // The reason for 'force' is to raise the chapter started event even if the
            // current chapter index is the same as the last chapter index, because although
            // the indexes are the same, the chapter itself is different, such as when the
            // chapter list has been reset.
            // However, even if force is true, we still dont force the started event if the
            // current chapter and the previously started chapter are null. 
            // Because even though the chapters are altered, there still is no current chapter.
            this._raiseChapterStarted(c);
        }
    },
    
    _ensureLoaded: function() {
        if (!this._loaded) {
            throw Error.invalidOperation(Sys.UI.Silverlight.MediaRes.silverlightNotLoaded);
        }
    },

    _ensureMedia: function() {
        // ensures the state of all the UI controls is consistent with the available media.
        // (1) whether media is available for playing, (2) if media supports seeking, (3) if media has a duration.
        // for example a live stream doesn't support seeking or has a duration, so next/back buttons and time readout
        // controls are hidden, but it supports play and stop so those are available. An ASX file may be set to
        // disallow seeking but it has a duration, so the TimeSlider is available but in readonly mode, etc...
        // This method is called any time any of the 3 attributes changes.
        // Since each control tracks whether it is already visible or already enabled, etc, it is safe to call
        // the setters even if the value is the same as it already is, it won't cause animations to play that shouldn't.
        var available = this._mediaAvailable;
        var hasDuration = (this._duration > 0);
        var canSeek = hasDuration && this._canSeek;
        var autoLoad = this.get_autoPlay() || this.get_autoLoad();
        var hasSource = !!this.get_mediaSource();
        // play button acts as a StartButton if there is no StartButton and autoLoad is disabled.
        var playAsStart = !this._children["StartButton"] && !autoLoad && hasSource;

        // If the chapter area is opened/activated and new media loads which is not seekable,
        // we should ensure the chapter area is deactivated. The reverse is not true -- media
        // loading which is seekable should not automatically activate the chapter area.
        // but it should make the chapter area available for activation, if it has items.
        this._setProperties("canActivate", ["ChapterArea"], canSeek);

        this._setProperties("readOnly", ["TimeSlider"], !canSeek);
        this._setProperties("enabled", ["TimeSlider"], hasDuration);        
        this._setProperties("visible", ["TotalTimeText", "CurrentTimeText"], hasDuration);        
        this._setProperties("enabled", ["PreviousButton", "NextButton"], canSeek);        
        this._setProperties("enabled", ["PauseButton", "StopButton"], available);
        this._setProperties("enabled", ["PlayPauseButton", "PlayButton"], available || playAsStart);
        
        var sb = this._children["StartButton"];
        if (sb) {
            var showSB = !available && !autoLoad && hasSource;
            sb.set_visible(showSB);
            sb.get_element().IsHitTestVisible = showSB;
        }

        if (hasDuration) {
            this._setProperties("text", ["TotalTimeText"], this._formatTime(this._duration));
        }        
    },
    
    _ensurePosition: function(seconds) {
        // Updates the time slider and current time text when the current position changes.
        if (this._duration) {
            // if there is no duration (e.g. live streaming) current time not displayed
            seconds = seconds || this.get_position();
            this._setProperties("text", ["CurrentTimeText"], this._formatTime(seconds));
            this._setProperties("value", ["TimeSlider"], seconds / this._duration);
        }
    },
    
    _formatTime: function(time) {
        // formats a time in seconds into HH:MM:SS format,
        // where HH: is omited if less than 1 hour, MM: is always present,
        // and each pair starts with a 0 if less than 10.
        var hours = Math.floor(time / 60 / 60);
        var minutes = Math.floor(time / 60) - (hours * 60);
        var seconds = Math.floor(time) - (minutes * 60) - (hours * 60 * 60);
        
        var s = "";
        if (hours) {
            hours = "0" + hours;
            s = hours.substr(hours.length-2, 2) + ":";
        }
        minutes = "0" + minutes;
        s += minutes.substr(minutes.length-2, 2) + ":";
        seconds = "0" + seconds;
        s += seconds.substr(seconds.length-2, 2);
        return s;
    },
    
    _getChapterAt: function(position) {
        // find the first chapter point with a position beyond the given position. The current chapter is the one
        // before that. May be -1 if the current position is less than the position of the very first chapter.
        for (var i = 0, l = this._timeline.length; i <= l; i++) {
            // Seeking in Silverlight is not as precise as a chapter point may be defined.
            // For example if you seek to position 5.1234, the actual position reported by silverlight
            // may be between 5.122 and 5.124, depending on the framerate of the media.
            // If a chapter point is 5.123 but setting this position results in 5.122, then it would appear
            // that the chapter has not yet started (not for another 0.001 seconds). This would cause
            // clicking on chapter #2, for example, to cause a chapter started event for chapter #1.
            // Therefore, we use a 0.001 second padding when comparing the timeline to the current position.
            // The position must be more than 0.001 seconds past a chapter point to be considered after it.
            if (i === this._timeline.length || ((this._timeline[i] - 0.001) > position)) return i-1;
        }
    },

    _loadPlaceholder: function() {
        var source = this.get_placeholderSource();
        var ph = this._children["PlaceholderImage"];
        if (ph && source) {
            ph.get_element().source = source;
            ph.set_visible(true);
        }
    },

    _meBufferingProgress: function() {
        if (!this._me) return; // may be called as dispose occurs
        var percent = Math.round(this._me.bufferingProgress * 100);
        this._enableBuffering(percent);
    },
    
    _mediaQueued: function() {
        // determines whether media is queued for download because a source exists but it
        // has not been set to the mediaelement
        return (!this.get_autoPlay() && !this.get_autoLoad()) && !this._me.source && this.get_mediaSource();
    },

    _meDoubleClick: function() {
        if (!this._me) return; // may be called as dispose occurs
        // full screen request came from double clicking the media, which caused it to toggle play/pause
        // toggle once again to put back
        this._onTogglePlayPause();
        this._onToggleFullScreen();
    },
    
    _meDownloadProgress: function() {
        if (!this._me) return; // may be called as dispose occurs
        // progressive download occuring, means there is no buffering. They don't occur at the same time.
        this._enableBuffering(null);
        this._setProperties("value", ["DownloadProgressSlider"], this._me.downloadProgress);
    },

    _meMarker: function(sender, args) {
        if (!this._me) return; // may be called as dispose occurs
        var marker = args.marker;
        if (this._toggledCaptions && this.get_enableCaptions()) {
            var type = marker.type ? marker.type.toLowerCase() : "";
            if (type === "caption") {
                var text = marker.text ? marker.text : "";
                if (text.trim().length === 0) {
                    // an all blank caption should turn off the caption
                    // but spaces at the end of a caption should be preserved
                    text = "";
                }
                this.set_caption(text);
                this.raisePropertyChanged("caption");
            }
        }

        var eventArgs = new Sys.UI.Silverlight.MarkerEventArgs(marker);
        this.onMarkerReached(eventArgs);
        this._raiseEvent("markerReached", eventArgs);
    },
    
    _meEnded: function() {
        if (!this._me) return; // may be called as dispose occurs
        this._mediaEnded = true;
        this._forcePlay = false;
        this.onMediaEnded(Sys.EventArgs.Empty);
        this._raiseEvent("mediaEnded");
    },

    _meFailed: function(sender, e) {   
        if (!this._me) return; // may be called as dispose occurs
        this._mediaAvailable = false;
        this._mediaEnded = false;
        this._canSeek = false;
        this._forcePlay = false;
        this._duration = 0;

        this._ensureMedia();
        this._enableBuffering(null);
        this.set_caption("");
        
        var args = new Sys.UI.Silverlight.ErrorEventArgs(e);
        this.onMediaFailed(args);
        this._raiseEvent("mediaFailed", args);
    },

    _meOpened: function() {
        if (!this._me) return; // may be called as dispose occurs
        this._mediaEnded = false;
        this._mediaAvailable = true;
        // Media.CanSeek and Media.NaturalDuration are cached even though it would be simpler to access
        // them directly when needed elsewhere because accessing these values from outside the MediaOpened
        // event is not as these properties were intended and may be unreliable. It is also better for
        // performance to avoid re-marshaling these values from the plugin instance.
        this._canSeek = this._me.canSeek;
        this._duration = this._me.naturalDuration.seconds;

        // volume may not have been set if previously ME was 'Closed'
        this._me.volume = this.get_volume();

        this._setProperties("visible", ["PlaceholderImage"], false);
        this.set_caption("");
        this._ensurePosition();
        this._ensureMedia();

        // hookup timer for monitoring time index (for chapter detection, time slider and currenttimetext updates)
        if (!this._timerCookie) {
            this._tickTimerDelegate = Function.createDelegate(this, this._tickTimer);
            this._timerCookie = window.setTimeout(this._tickTimerDelegate, 200);
        }
        
        this.onMediaOpened(Sys.EventArgs.Empty);
        this._raiseEvent("mediaOpened");
        
        if (this._forcePlay) {
            this._forcePlay = false;
            this._me.play();
        }
    },
    
    _meState: function() {
        if (!this._me) return; // may be called as dispose occurs
        var state = this._me.currentState;
        
        if (state === "Stopped" && this._forcePlayOnStop) {
            this._forcePlayOnStop = false;
            this._me.play();
        }
        
        if (state === this._oldState) return;
        this._oldState = state;
        
        if (state === "Closed") {
            if (this._mediaAvailable) {
                // Only shut down on Closed if media is previously available,
                // because Silverlight can sometimes raise the Closed state while opening the media.
                this._enableBuffering(null);
                this._mediaAvailable = false;
                this._canSeek = false;
                this._forcePlay = false;
                this._duration = 0;
                this._mediaEnded = false;
                this._ensureMedia();
                this.set_caption("");
            }
        }
        else if (state === "Playing" || state === "Paused") {
            // playing/paused, make sure buffering area is hidden
            // Silverlight sometimes begins playing before Buffering reaches 100%
            this._enableBuffering(null);
        }
        
        this._setProperties("state", ["PlayPauseButton"], state === "Playing" ? 1 : 0);
        this.onCurrentStateChanged(Sys.EventArgs.Empty);
        this._raiseEvent("currentStateChanged");
    },
    
    _onCaptionToggle: function() {
        this._toggledCaptions = !this._toggledCaptions;
        this._ensureCaption();
        this._setProperties("state", ["CaptionToggleButton"], this._toggledCaptions ? 0 : 1);
    },
    
    _onChapterClick: function(index) {
        var chapters = this.get_chapters();
        if (index < chapters.length) {
            // it should be impossible to click on a chapter out of range,
            // but a malformed skin that displays image list items that aren't in use could
            // allow the user to do so. Only honor clicks that correspond to actual chapters.
            var chapter = chapters[index];
            var args = new Sys.UI.Silverlight.MediaChapterEventArgs(chapter);
            this.onChapterSelected(args);
            this._raiseEvent("chapterSelected", args);
            if (!args.get_cancel()) {
                this.set_currentChapter(chapter);
            }
        }
    },

    onChapterSelected: function(args) {
        /// <summary>Called when the user navigates to a chapter point.</summary>
        /// <param name="args" type="Sys.UI.Silverlight.MediaChapterEventArgs">
        ///     Contains the chapter that was selected.
        /// </param>
    },
    
    onChapterStarted: function(args) {
        /// <summary>Called when the current chapter has changed.</summary>
        /// <param name="args" type="Sys.UI.Silverlight.MediaChapterEventArgs">
        ///     Contains the chapter that is starting.
        /// </param>
    },
    
    onCurrentStateChanged: function(args) {
        /// <summary>Called when the state of the currently opened media changes.</summary>
        /// <param name="args" type="Sys.UI.EventArgs">Empty.</param>
    },
    
    onMarkerReached: function(args) {
        /// <summary>Called when a marker embedded in the media has been reached.</summary>
        /// <param name="args" type="Sys.UI.Silverlight.MarkerEventArgs">Information about the marker that was reached.</param>
    },

    onMediaEnded: function(args) {
        /// <summary>Called when the media reached the end.</summary>
        /// <param name="args" type="Sys.UI.EventArgs">Empty.</param>
    },

    onMediaFailed: function(args) {
        /// <summary>Called when media fails to open.</summary>
        /// <param name="args" type="Sys.UI.EventArgs">Empty.</param>
    },

    onMediaOpened: function(args) {
        /// <summary>Called when media successfully opens.</summary>
        /// <param name="args" type="Sys.UI.EventArgs">Empty.</param>
    },
    
    _onMute: function() {
        this.set_muted(!this.get_muted());
    },
    
    _onNext: function() {
        var chapters = this.get_chapters();
        if (!chapters || !chapters.length) {
            // if chapters are not defined, next goes forward 10% of duration
            this._skipTime(1);
        }
        else {
            // if chapters are defined, next goes forward one chapter.
            var next = this._chapterStarted + 1;
            // if already on the last chapter, do nothing
            if (next < chapters.length) {
                var chapter = chapters[next];
                var args = new Sys.UI.Silverlight.MediaChapterEventArgs(chapter);
                this.onChapterSelected(args);
                this._raiseEvent("chapterSelected", args);
                if (!args.get_cancel()) {
                    this.set_currentChapter(chapter);
                }
            }
        }
    },
    
    _onPause: function() {
        this.pause();
    },
    
    _onPlay: function() {
        this.play();
    },

    onPluginFullScreenChanged: function(args) {
        /// <summary>Called when full screen mode is toggled.</summary>
        /// <param name="args" type="Sys.EventArgs">Empty.</param>
        
        // FullScreen mode works by activating a "FullScreenArea" Canvas in the xaml especially for full screen mode.
        // This canvas contains a FullScreenVideoWindow Canvas which has a VideoBrush applied to it, whose source is
        // the normal-mode MediaElement.
        // When full screen begins, we must scale up the FullScreenArea Canvas to fill the entire screen. However,
        // this ignores aspect ratio constraints. For example, the user may have a wide screen monitor. So in the
        // process we may have distorted the aspect ratio of the FullScreenVideoWindow Cavans, because it is a
        // child canvas. So we then apply a MatrixTransform to the FullScreenVideoWindow Canvas which brings it back
        // to its original aspect ratio, but scaled to fill as much of the the FullScreenArea as possible, and
        // centers it horizontally and vertically within FullScreenArea.

        var fs = this._children["FullScreenArea"];
        if (!fs) return;
        fs = fs.get_element();
        var content = this.get_element().content, root = content.root;
        
        if (content.FullScreen) {
            // Beginning of FullScreen mode. Show the full screen area.
            fs.Visibility = 0;
            
            var offsetX = 0, offsetY = 0,
                scaleMode = this.get_scaleMode(),
                scale = Sys.UI.Silverlight.Control._computeScale(root, scaleMode);
            if (scaleMode !== Sys.UI.Silverlight.ScaleMode.stretch) {
                // center the result horizontally and vertically
                var minScale = Math.min(scale.horizontal, scale.vertical);
                offsetX = (content.ActualWidth-fs.width*minScale) / 2;
                offsetY = (content.ActualHeight-fs.height*minScale) / 2;
            }
            this._originalScale = Sys.UI.Silverlight.Control._applyMatrix(root, scale.horizontal, scale.vertical, offsetX, offsetY);
            // entire plugin is now stretched to fill the screen according to the scale mode,
            // and centered horizontally and vertically if the mode is Zoom.
        }
        else {
            // important to reapply the scale as it was before fullscreen mode, because the control
            // may be set to ScaleMode = none. In that case if we didn't do this we'd be leaving the
            // root canvas at the size of fullscreen. 
            Sys.UI.Silverlight.Control._applyMatrix(root, this._originalScale.horizontal, this._originalScale.vertical, 0, 0);
            // End of FullScreen mode. Hide the fullscreen area.
            // no need to undo transformed applied since the element is disappearing.
            fs.Visibility = 1;
        }        
    },

    _onPrevious: function() {
        var chapters = this.get_chapters();
        if (!chapters || !chapters.length) {
            // if chapters are not defined, previous goes back 10% of duration
            this._skipTime(-1);
        }
        else {
            // if chapters are defined, previous goes back one chapter.
            // Clicking the previous chapter button ...
            //      In the middle of chapter #X should start at the beginning of chapter #X.
            //      At the start of or within 1 second of the start of chapter #X should go back to chpater #X-1
            // note: We could just use _getChapterAt to find what the chapter was at CurrentPosition-1.
            //       But it is possible that would skip past more than 1 chapter if a chapter is very short.
            var newChapter = -1;
            // if the current chapter is -1, just skip to the beginning
            if (this._chapterStarted >= 0) {
                var startTime = this._timeline[this._chapterStarted];
                var position = this.get_position();
                if ((position - startTime) > 1) {
                    // more than 1 second into the current chapter, just restart the current chapter
                    newChapter = this._chapterStarted;
                }
                else {
                    // less than 1 second into the current chapter, go to the previous chapter
                    newChapter = this._chapterStarted-1;
                }
            }

            var chapter = (newChapter === -1) ? null : chapters[newChapter];
            var args = new Sys.UI.Silverlight.MediaChapterEventArgs(chapter);
            this.onChapterSelected(args);
            this._raiseEvent("chapterSelected", args);
            if (!args.get_cancel()) {
                if (chapter) {
                    this.set_currentChapter(chapter);
                }
                else {
                    // chapter === -1 means skip back to the beginning (the first chapter has position > 0)
                    this.set_position(0);
                }
            }
        }
    },

    _onStop: function() {
        this.stop();
    },
    
    _onToggleFullScreen: function() {
        var c = this.get_element().content;
        c.FullScreen = !c.FullScreen;
    },
    
    _onTogglePlayPause: function() {
        this.get_currentState() === "Playing" ? this.pause() : this.play();
    },
    
    _onTimeChanged: function(slider) {
        this.set_position(slider.get_value() * this._duration);    
    },

    onVolumeChanged: function(args) {
        /// <summary>Called when the volume level changes.</summary>
        /// <param name="args" type="Sys.EventArgs">Empty.</param>
    },
    
    _onVolumeChanged: function(slider) {
        this.set_volume(slider.get_value());
    },
    
    _onVolumeDown: function() {
        this.set_volume(Math.max(0, this.get_volume() - 0.02));    
    },

    _onVolumeUp: function() {
        // each click of the volume buttons changes volume by 2%
        this.set_volume(Math.min(1, this.get_volume() + 0.02));    
    },

    pause: function() {
        /// <summary>Pause the media.</summary>
        this._ensureLoaded();
	    this._me.pause();
    },

    play: function() {
        /// <summary>Start playing the media.</summary>
        this._ensureLoaded();
        if (this._mediaQueued()) {
            // hitting play while media is queued causes it to be set to the MediaElement
            // when it loads, it will play regardless of the autoplay setting
            this._forcePlay = true;
            this._me.source = this.get_mediaSource();
            // hide the StartButton immediately
            var sb = this._children["StartButton"];
            if (sb) {
                sb.set_visible(false);
                sb.get_element().IsHitTestVisible = false;
            }
        }
        else {
            if (this._mediaEnded) {
                this._mediaEnded = false;
                // user has hit play but the media already played to the end.
                // SL doesn't automatically rewind, so we must call stop first so it starts over.
                this.set_caption("");
                
                // wait until the state is Stopped because calling Play
                // immediately after Stop does not always work, even with a setTimeout
                this._forcePlayOnStop = true;
                this._me.stop();
            }
            else {
                this._me.play();
            }
        }
    },
    
    pluginDispose: function() {
        if (this._timerCookie) {
            window.clearTimeout(this._timerCookie);
            this._timerCookie = null;
        }
        for (var child in this._children) {
            this._children[child].dispose();
        }
        if (this._me) {
            this._me.stop();
            this._me = null;
        }
        if (this._bufferingStoryboard) {
            this._bufferingStoryboard.stop();
            this._bufferingStoryboard = null;
        }
        Sys.UI.Silverlight.MediaPlayer.callBaseMethod(this, "pluginDispose");        
    },    
    
    _raiseChapterStarted: function(index) {
        this._chapterStarted = index;
        var chapter = index === -1 ? null : this.get_chapters()[index];
        var args = new Sys.UI.Silverlight.MediaChapterEventArgs(chapter);
        this.onChapterStarted(args);
        this._raiseEvent("chapterStarted", args);
    },
    
    _raisepluginLoaded: function() {
        Sys.UI.Silverlight.MediaPlayer.callBaseMethod(this, "_raisepluginLoaded");
        this._bindAllControls(); 

        // copy cached property values into the controls that weren't available at the time
        var me = this._me;
        this._loadPlaceholder();
        me.autoPlay = this.get_autoPlay();
        
        me.isMuted = this.get_muted();
        this._setProperties("state", ["MuteButton"], this.get_muted() ? 1 : 0);
        
        me.volume = this.get_volume();
        this._setProperties("value", ["VolumeSlider"], this.get_volume());
        
        this._setProperties("items", ["ChapterArea"], this.get_chapters());
        this._ensureCaption();

        if (this.get_mediaSource() && (this.get_autoPlay() || this.get_autoLoad())) {
            // media is loading, the opened/failed/closed event will setup the player controls appropriately
            me.source = this.get_mediaSource();
        }
        else {
            // no media will be loading initially, player controls disabled
            this._ensureMedia();
        }
    },    
    
    _setProperties: function(name, children, value) {
        // sets the same property of multiple children if they exist to the same value.
        // saves significant space inlining the logic inside the for loop
        for (var i = 0, l = children.length; i < l; i++) {
            var c = this._children[children[i]];
            if (c) {
                c["set_" + name](value);
            }
        }
    },
    
    _skipTime: function(direction) {
        // skipping always skips by 10% of the total duration
        // but not less than 5 seconds.
        var delta = Math.max(5, this._duration / 10);
        delta = direction * delta;
        var newTime = delta + this.get_position();
        this.set_position(newTime);
    },
    
    stop: function() {
        /// <summary>Stop the media.</summary>
        this._ensureLoaded();
	    this._me.stop();
	    this._ensurePosition();
        this._mediaEnded = false;
	    this.set_caption("");
    },
    
    _tickTimer: function() {
        this._timerCookie = window.setTimeout(this._tickTimerDelegate, 200);
        var position = this.get_position();
        // detect changes in the current chapter point
        this._detectChapterChange(position);
        if (this._forceUpdate || (this.get_currentState() === "Playing")) {
            this._forceUpdate = false;
            this._ensurePosition(position);
        }
    }
}
Sys.UI.Silverlight.MediaPlayer.registerClass('Sys.UI.Silverlight.MediaPlayer', Sys.UI.Silverlight.Control);


Type.registerNamespace('Sys.UI.Silverlight');Sys.UI.Silverlight.MediaRes={"noMediaElement":"The Silverlight source must contain a MediaElement named \"VideoWindow\".","volumeRange":"Volume must be a number greater than or equal to 0 and less than or equal to 1.","noThumbElement":"Could not find thumb element \u0027{1}\u0027 for slider with name \u0027{0}\u0027.","invalidChapter":"Chapter must be one of the chapters assigned to the chapters property.","silverlightNotLoaded":"This operation cannot be performed prior to the client \u0027silverlightLoaded\u0027 event."};
if(typeof(Sys)!=='undefined')Sys.Application.notifyScriptLoaded();