Shotwell Architecture: Media and Media Interfaces
Shotwell 0.8 introduced video support, the first non-photographic media type. Video was considered distinct enough that our implementation treats it as a separate data type, rather than a special case of Photo. This opens the door to the possibility of Shotwell managing (i.e. organizing) other media types, including audio, PDFs, and more.
1. MediaSource and MediaSourceCollection
New members of the data architecture are MediaSource and MediaSourceCollection. These classes live between Photo and Video and their more generic ancestor DataSource. There is parallel parentage between SourceCollection and LibraryPhotoSourceCollection / VideoSourceCollection.
There is a classic object-oriented tension presented here. One impulse is to treat the child classes -- Photo and Video -- as utterly separate types, as they are unlike in many ways. However, this is unrealistic because (a) they do have some common characteristics and properties, and (b) there is a desire to organize them together, i.e. placing photos and videos in the same Event or tagging them with the same keyword. To treat them totally separately means a lot of special-case code sprinkled throughout the program: If it's a photo, do this, otherwise it's a video, so do that.
The other impulse is to treat all media as generic objects and leave the details to the subclasses. This has the advantage of removing all special-case code. For example, the Rotate button simply calls the rotate() method on the media object. This is another form of simplicity, but because Photos and Videos are also quite different (we do not support video rotation, for example), this means lots of empty stubs. In this case, Video.rotate() would simply return false. Because it's desirable to reflect valid operations in the user interface, a second set of interfaces is required to query objects for capabilities: Can you rotate? This simply replaces one kind of special-case code with another: _If the MediaSource is a Photo, rotate it_ versus _If the MediaSource can_rotate(), rotate it_.
We opted for the first case in our initial stab at video, although we did place quite a bit of common code and abstract methods in MediaSource as a nod to the shared characteristics of all media and how we organize it. But going forward, will this scale?
2. MediaInterfaces
Vala has a flexible notion of what an interface is, something more akin to mixins than traditional Java/C# interfaces. In particular, Vala interfaces can specify requisite interfaces and even a requisite base class. For example, if we made a Streamable interface, it could be specified like this:
public interface Streamable : MediaSource
Thus, any code dealing with Streamable know they're dealing with MediaSources and have access to all its methods.
This means we can selectively add features to MediaSources that only apply to a subset of the actual concrete classes (such as Streamable, which applies to video and audio but not photos). Thus, code can determine on the fly if the MediaSource it's dealing with knows its playing time, rather than If the MediaSource is a Video or is Audio, get its playing time. If we added Presentations later, this code would have to be updated. If Presentations is Streamable (which it is!), it's good to go.
This also means functionality can be added to new media types incrementally. One problem we had with video was that, in order for Shotwell to build and operate in trunk, a lot of functionality had to be coded at once and checked-in likewise. With the MediaInterfaces approach, functionality could be added incrementally by adding new interfaces one at a time and coding to them.
3. In Practice
Today, MediaInterfaces are not widely used. Flags and monitoring were added by coding Flaggable and Monitorable interfaces. Future media features should be added by first coding an interface and then implementing them for each media type.
Time permitting, it would be good to go back and code new interfaces for various features: Trashable and Rotatable, for example. (Since video rotation is on the table, this is not such a strange proposal or a one-off interface.)
Container objects -- tags, events, and the like -- should start using interfaces as well: Taggable, Eventable, Recognizable (for faces), and so on. Thus, Tag deals in Taggable and Event with Eventables.