HTML5 Media, WebDriver and Python

So again I was chatting with Jim Holmes and he asked me if I knew how to script HTML5’s video tag. I had never even looked at it so had a look. One thing lead to another to another and I had some code. But before I get that;

  • The video tag is a Media Element
  • So is the audio tag
  • Actually, the implementation of an audio tag is exactly the same as the base media element type
  • The video tag has a viewable section so has dimensions aside from those specified as media elements
  • Interaction with the video and audio element is done entirely through JavaScript
  • Which means that there is bound to be differences between browsers as they interpret the standards slightly different
  • There are also a growing number of non-native implementations of players which override the native implementations and will need their own WebDriver implementation

Rather than paste the entire code for dealing with HTML5 media elements, I’ll just explain some of the important parts.

I hope to convince David Burns to include this as part of the Python WebDriver bindings as part of the support package (I suspect I need to write tests for it and document it first) so I took a queue from the Select class in creating the constructor.

class Video(HTML5Media):
    def __init__(self, webelement):
        """
        Constructor. A check is made that the given element is, indeed, a VIDEO tag. If it is not,
        then an UnexpectedTagNameException is thrown.
 
        :Args:
         - webelement - element VIDEO element to wrap
 
        Example:
            from selenium.webdriver.support.ui import Select \n
            Video(driver.find_element_by_tag_name("video")).play()
        """
        if webelement.tag_name.lower() != "video":
            raise UnexpectedTagNameException(
                "Video only works on <video> elements, not on <%s>" % 
                webelement.tag_name)
        self._el = webelement
        self.changed = False

My initial thought was to include this sort of tag inclusion in the find_by_* methods but the diversity of third party players made me rethink this.

Next, remember than control of these elements is through JavaScript. This means that that any WebDriver control is going to be through the JavaScript Executor interface. One feature of the executor is that you can pass in a WebDriver WebElement object as an argument and it is available [somehow] to the JavaScript being run.

@property
@stale
def loop(self):
    return self._el._parent.execute_script("return arguments[0].loop", self._el)
 
@loop.setter
def loop(self, value):
    if isinstance(value, bool):
        self._el._parent.execute_script("arguments[0].loop = arguments[1]", self._el, value)
        self.changed = True
    else:
        raise ValueError("value needs to be a boolean")

A few things to point out…

  • Script execution is done at the page, not element level so have to use _parent which is the actual driver instance
  • When you manipulate (or have manipulated) an element in WebDriver, normally it will trigger a Stale Element Exception, but the manipulations this way were not triggering it, so I had to cobble it in with the self.changed stuff
  • Rather than have a check in every property I created a @stale decorator to do it

So what does it look like in a script?

from selenium import webdriver
 
driver = webdriver.Firefox()
 
driver.get("http://html5demos.com/video")
e = driver.find_element_by_css_selector("video")
v = Video(e)
v.play()
print("Ended?: {0}".format(v.ended))
 
driver.quit()

Easy-peasy.

As I mentioned, I hope to get this into the official distro, but until then it is up on github

Post a Comment

Your email is never published nor shared. Required fields are marked *