Cache Plug-in Implementation of ISmoothStreamingCache

The Smooth Streaming Client provides the ISmoothStreamingCache interface to support offline scenarios. An implementation of the cache is started by assigning it to the SmoothStreamingCache property of the SmoothStreamingMediaElement class. When the cache is started and the SmoothStreamingMediaElement instance downloads a manifest or data chunk, the BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) and BeginRetrieve(CacheRequest, AsyncCallback, Object) methods provide options to save data for future use and to use cached data instead of downloading from the network. When the SmoothStreamingMediaElement instance requires a chunk or manifest, it first calls BeginRetrieve(CacheRequest, AsyncCallback, Object) to determine whether the assigned cache has the data. If the cache has the data, a data chunk from the cache is used. Otherwise, the SmoothStreamingMediaElement instance downloads the data using HTTP. After the response is downloaded, the SmoothStreamingMediaElement instance calls BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) to save the downloaded chunk.

The cache implementation is independent of any particular codec requirements. The SmoothStreamingMediaElement instance makes the same requests to the cache that it would to the IIS server. A cache implementation can have a downloader that reads the client manifest and mimics the SmoothStreamingMediaElement object by making requests to the server and storing the responses in the cache. When the SmoothStreamingMediaElement instance makes requests to the cache, the cache responds with the pre-downloaded data that has been serialized in the cache.

Implementation of ISmoothStreamingCache

This topic shows how to create an class that implements the ISmoothStreamingCache interface for Smooth Streaming Client offline scenarios. The application persists data in an instance of System.IO.IsolatedStorage.IsolatedStorage and uses URL/file name pairs in the IsolatedStorageSettings.ApplicationSettings object to track manifests and data chunks that have been saved to the cache. The URL/file name pairs stored in the IsolatedStorageSettings.ApplicationSettings object are read into a dictionary object when the cache is instantiated, and the dictionary pairs are used to identify data files in the cache.

The sample demonstrates implementations of the four methods of ISmoothStreamingCache:

When an ISmoothStreamingCache object is started and a request for data is issued, the Silverlight Smooth Streaming Client will call each of the methods in the order: BeginRetrieve, EndRetrieve, BeginPersist, EndPersist.

On the first pass, before the cache has any data, the calls to BeginRetrieve(CacheRequest, AsyncCallback, Object) and EndRetrieve(IAsyncResult) return a null CacheResponse object. The application then calls BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) to cache the data for future use as required for the offline scenario. The EndPersist(IAsyncResult) method parses the response data returned from the server, and if there is enough available storage, the data is stored in an System.IO.IsolatedStorage.IsolatedStorage instance.

If BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) and EndPersist(IAsyncResult) have previously run successfully, the cache will already have data when a request is made. Instead of making a call to the IIS server, BeginRetrieve(CacheRequest, AsyncCallback, Object) processes (and EndRetrieve(IAsyncResult) returns) a non-null CacheResponse object to be played back by the application instance of SmoothStreamingMediaElement.

The following code shows a complete implementation of the class that implements ISmoothStreamingCache. The code is discussed in sections that follow the example.

    public class ISO_StorageCache : ISmoothStreamingCache
    {
        // Dictionary to track URL/filename pairs of data in cache.
        public Dictionary<string, string> keyUrls = new Dictionary<string, string>(50);

        public ISO_StorageCache()
        {
            IsolatedStorageFile isoFileArea = IsolatedStorageFile.GetUserStoreForApplication(); 

            foreach (System.Collections.Generic.KeyValuePair<string, object> pair in IsolatedStorageSettings.ApplicationSettings)
            {
                if (!keyUrls.ContainsValue((string)pair.Value) && isoFileArea.FileExists((string)pair.Value))
                    keyUrls.Add(pair.Key, ((string)pair.Value));
            }
        }

        public IAsyncResult BeginRetrieve(CacheRequest request, AsyncCallback callback, object state)
        {
            CacheResponse response = null;
            CacheAsyncResult ar = new CacheAsyncResult();
            ar.strUrl = request.CanonicalUri.ToString();
            ar.Complete(response, true);
            return ar; 
        }        

        public CacheResponse EndRetrieve(IAsyncResult ar)
        {
            ar.AsyncWaitHandle.WaitOne();

            CacheResponse response = null;

            if (keyUrls.ContainsKey(((CacheAsyncResult)ar).strUrl))
            {
                IsolatedStorageFile isoFileArea = IsolatedStorageFile.GetUserStoreForApplication();
                string filename = keyUrls[((CacheAsyncResult)ar).strUrl];

                if (!string.IsNullOrEmpty(filename) && isoFileArea.FileExists(filename))
                {
                    IsolatedStorageFileStream stream = 
                        isoFileArea.OpenFile(filename, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                    response = new CacheResponse(stream); 
                }
            }

            if (response != null)
                return response;
            else
                return response = new CacheResponse(0, null, null, null,
                    System.Net.HttpStatusCode.NotFound, "Not Found", TimeManager.Now);
        }

        public IAsyncResult BeginPersist(CacheRequest request, CacheResponse response, AsyncCallback callback, object state)
        {
            state = false;
            CacheAsyncResult ar = new CacheAsyncResult();

            if (!keyUrls.ContainsKey(request.CanonicalUri.ToString()))
            {
                //state = true;
                ar.strUrl = request.CanonicalUri.ToString();
                ar.Complete(response, true);
                return ar;
            }

            ar.Complete(null, true);
            return ar;
        }

        public bool EndPersist(IAsyncResult ar)
        {
            ar.AsyncWaitHandle.WaitOne();

            if (((CacheAsyncResult)ar).Result != null)
            {
                IsolatedStorageFile isoFileArea = IsolatedStorageFile.GetUserStoreForApplication();

                if (((CacheResponse)(((CacheAsyncResult)ar).Result)).Response.Length < isoFileArea.AvailableFreeSpace)
                {
                    string fileGuid = Guid.NewGuid().ToString();
 
                    if (!keyUrls.ContainsValue(fileGuid) && !keyUrls.ContainsKey(((CacheAsyncResult)ar).strUrl))
                    {
                        
                        IsolatedStorageFileStream isoFile = isoFileArea.CreateFile(fileGuid);

                        ((CacheResponse)(((CacheAsyncResult)ar).Result)).WriteTo(isoFile);
                        isoFile.Close(); 

                        keyUrls.Add(((CacheAsyncResult)ar).strUrl, fileGuid);
                        // Save key/value pairs for playback after application restarts.
                        IsolatedStorageSettings.ApplicationSettings.Add(((CacheAsyncResult)ar).strUrl, fileGuid);
                        IsolatedStorageSettings.ApplicationSettings.Save();

                        return true;
                    }
                }
            }
            return false;
        }
    }

BeginRetrieve Method

The following code shows the implementation of BeginRetrieve(CacheRequest, AsyncCallback, Object) used by this cache implementation.

public IAsyncResult BeginRetrieve(CacheRequest request, AsyncCallback callback, object state)
{
    CacheResponse response = null;
    CacheAsyncResult ar = new CacheAsyncResult();
    ar.strUrl = request.CanonicalUri.ToString();
    ar.Complete(response, true);
    return ar; 
}

The request parameter of the BeginRetrieve(CacheRequest, AsyncCallback, Object) method contains the URL that the SmoothStreamingMediaElement instance has requested. In order for the method to retrieve a data file from the cache, this URL has to match a key in the keysUrls member variable. The keysUrls member is a dictionary object that contains the URLs and file names used to track data files in the cache. The code in BeginRetrieve(CacheRequest, AsyncCallback, Object) assigns the URL of the request to the strURL member of an instance of CacheAsynResult so that it can be passed to EndRetrieve(IAsyncResult). If the URL matches a key in the keysUrls dictionary, the EndRetrieve(IAsyncResult) method gets the corresponding data file. The CacheAsyncResult class implements System:IAsyncResult for this application. For more information about CacheAsyncResult, see IAsyncResult Implementation.

EndRetrieve Method

The EndRetrieve(IAsyncResult) method gets the URL of the request from the CacheAsyncResult instance returned by BeginRetrieve(CacheRequest, AsyncCallback, Object). It is important to wait for BeginRetrieve(CacheRequest, AsyncCallback, Object) to complete before processing the data, as shown by the call to ar.AsyncWaitHandle.WaitOne. If there is data, it will be returned to the SmoothStreamingMediaElement instance. If the cache does not have the data, EndRetrieve(IAsyncResult) returns a CacheResponse instance and a System.Net.HttpStatusCode.NotFound value, as shown at the end of the following implementation of EndRetrieve(IAsyncResult).

    public CacheResponse EndRetrieve(IAsyncResult ar)
    {
        ar.AsyncWaitHandle.WaitOne();

        CacheResponse response = null;

        if (keyUrls.ContainsKey(((CacheAsyncResult)ar).strUrl))
        {
            IsolatedStorageFile isoFileArea = IsolatedStorageFile.GetUserStoreForApplication();
            string filename = keyUrls[((CacheAsyncResult)ar).strUrl];

            if (!string.IsNullOrEmpty(filename) && isoFileArea.FileExists(filename))
            {
                IsolatedStorageFileStream stream = 
                    isoFileArea.OpenFile(filename, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                response = new CacheResponse(stream);
            }
        }

        if (response != null)
            return response;
        else
            return response = new CacheResponse(0, null, null, null,
                System.Net.HttpStatusCode.NotFound, "Not Found", TimeManager.Now);
    }

The first time BeginRetrieve(CacheRequest, AsyncCallback, Object) and EndRetrieve(IAsyncResult) are called, the cache will not contain data, and the block of code contained by the first if statement of the EndRetrieve(IAsyncResult) method is skipped. The CacheResponse object will remain null, and EndRetrieve(IAsyncResult) will return an empty CacheResponse object with a System.Net.HttpStatusCode.NotFound exception.

If the cache contains data for the requested media chunk or manifest, the value in strUrl will match one of the URL/file name pairs in the keyUrls dictionary object. When processing enters the first if block, data is persisted as shown in the discussion of EndPersist(IAsyncResult) later in this document.

Silverlight applications use the System.IO.IsolatedStorage.IsolatedStorage class to persist application data because online applications do not have read/write access to storage on users’ computers. The code gets the storage area and file that contain the data identified by the keyUrls[((CacheAsyncResult)ar).strUrl] value. Casting is required in order to get the URL string from the asynchronous result.

Code in the second if block verifies that the file name matches an existing file in isolated storage. When this block runs, the code reads the System.IO.IsolatedStorage.IsolatedStorageFileStream object into a new CacheResponse object named response.

After the code in the second if block runs, the CacheResponse object (response) contains data, and the method returns the response object to the SmoothStreamingMediaElement instance either as manifest information or as an audio or video chunk.

BeginPersist Method

The BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) method begins the process that adds data to the cache. When an implementation of ISmoothStreamingCache is assigned to the SmoothStreamingCache property of the SmoothStreamingMediaElement object, the SmoothStreamingMediaElement object calls the BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) method right after calling the EndRetrieve(IAsyncResult) method.

    public IAsyncResult BeginPersist(CacheRequest request, CacheResponse response, AsyncCallback callback, object state)
    {
        CacheAsyncResult ar = new CacheAsyncResult();
        state = false;

        if (!keyUrls.ContainsKey(request.CanonicalUri.ToString()))
        {
            state = true;
            ar.strUrl = request.CanonicalUri.ToString();
            ar.Complete(response, (bool)state);
            return ar;
        }

        ar.Complete(null, true);
        return ar;
    }

This implementation of BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) creates a new instance of CacheAsyncResult that will be passed to EndPersist(IAsyncResult). If the request.CanonicalUri value does not match a key in the dictionary of URLs already in the cache, the URL is assigned to the strUrl property of the CacheAsyncResult object and returned to be processed further by EndPersist(IAsyncResult).

The response parameter of BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) contains the data to persist. This data is supplied as the first argument of the CacheAsyncResult.Complete method. The second argument is null because the BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) method is always called synchronously by SmoothStreamingMediaElement.

EndPersist Method

The EndPersist(IAsyncResult) method waits for the BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) method to complete. Both EndRetrieve(IAsyncResult) and EndPersist(IAsyncResult) are blocking calls. You can call them at any time and they return only when the operation is completed.

The first if condition determines whether the CacheAsyncResult contains data. If it does, the next line of code gets the System.IO.IsolatedStorage.IsolatedStorageFile object named isoFileArea with which the application persists data.

    public bool EndPersist(IAsyncResult ar)
    {
        ar.AsyncWaitHandle.WaitOne();

        if (((CacheAsyncResult)ar).Result != null)
        {
            IsolatedStorageFile isoFileArea = IsolatedStorageFile.GetUserStoreForApplication();

            if (((CacheResponse)(((CacheAsyncResult)ar).Result)).Response.Length < isoFileArea.AvailableFreeSpace)
            {
                string fileGuid = Guid.NewGuid().ToString();
 
                if (!keyUrls.ContainsValue(fileGuid) && !keyUrls.ContainsKey(((CacheAsyncResult)ar).strUrl))
                {
                        
                    IsolatedStorageFileStream isoFile = isoFileArea.CreateFile(fileGuid);

                    ((CacheResponse)(((CacheAsyncResult)ar).Result)).WriteTo(isoFile);
                    isoFile.Close(); 

                    keyUrls.Add(((CacheAsyncResult)ar).strUrl, fileGuid);
                    // Save key/value pairs for playback after application restarts.
                    IsolatedStorageSettings.ApplicationSettings.Add(((CacheAsyncResult)ar).strUrl, fileGuid);
                    IsolatedStorageSettings.ApplicationSettings.Save();

                    return true;
                }
            }
        }
        return false;
    }

The values in the second if block require casting to get the length of the response data (((CacheResponse)(((CacheAsyncResult)ar).Result)).Response.Length). If the response data fits in the application's isolated storage area, a new IsolatedStorage.IsolatedStorageFileStream object is created and assigned a name using a GUID in order to ensure uniqueness. The code opens an IsolatedStorage.IsolatedStorageFileStream instance and writes the response data to a file. A URL/file name pair is added to the keyUrls dictionary to identify the data for a subsequent call to the BeginRetrieve(CacheRequest, AsyncCallback, Object) method.

IAsyncResult Implementation

The following code shows an implementation of IAsyncResult. An instance of this class is passed from the BeginRetrieve(CacheRequest, AsyncCallback, Object) method to EndRetrieve(IAsyncResult) and from the BeginPersist(CacheRequest, CacheResponse, AsyncCallback, Object) method to EndPersist(IAsyncResult).

    public class CacheAsyncResult : IAsyncResult
    {
        public string strUrl { get; set; }

        public object AsyncState { get; private set; }

        public WaitHandle AsyncWaitHandle { get { return _completeEvent; } }

        public bool CompletedSynchronously { get; private set; }

        public bool IsCompleted { get; private set; }

        // Contains the output result of the GetChunk API
        public Object Result { get; private set; }

        internal TimeSpan Timestamp { get; private set; }

        // Callback function when GetChunk is completed. Used in asynchronous mode only.
        // Null for synchronous mode.
        private AsyncCallback _callback;

        // Event is used to signal the completion of the operation
        private ManualResetEvent _completeEvent = new ManualResetEvent (false);

        // Called when the operation is completed
        public void Complete(Object result, bool completedSynchronously)
        {
            Result = result;
            CompletedSynchronously = completedSynchronously;

            IsCompleted = true;
            _completeEvent.Set();

            if (null != _callback) { ;  }
        }

See Also

Concepts

Test and Debug an Implementation of ISmoothStreamingCache

Other Resources

System.IO.IsolatedStorage Namespace