[rygel] server: Serve file items using resources
- From: Jens Georg <jensgeorg src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [rygel] server: Serve file items using resources
- Date: Sun, 8 Feb 2015 13:25:33 +0000 (UTC)
commit d0c9a4e5c2389b25c2c811e6fa666fed3626e644
Author: Jens Georg <mail jensge org>
Date: Thu Nov 20 17:45:54 2014 +0100
server: Serve file items using resources
Based on Cablelabs's CVP-2 implementation.
Signed-off-by: Jens Georg <mail jensge org>
src/librygel-server/rygel-audio-item.vala | 9 +-
src/librygel-server/rygel-http-get-handler.vala | 7 +-
src/librygel-server/rygel-http-get.vala | 22 +++
.../rygel-http-identity-handler.vala | 4 +
.../rygel-http-resource-handler.vala | 4 +
src/librygel-server/rygel-http-server.vala | 26 ----
.../rygel-http-transcode-handler.vala | 4 +
src/librygel-server/rygel-image-item.vala | 23 ++--
src/librygel-server/rygel-media-engine.vala | 23 +++
src/librygel-server/rygel-media-file-item.vala | 142 ++++++++++++++++----
src/librygel-server/rygel-media-object.vala | 2 +-
src/librygel-server/rygel-video-item.vala | 13 +--
src/librygel-server/rygel-visual-item.vala | 2 +-
.../gstreamer/rygel-gst-media-engine.vala | 61 +++++++++
.../simple/rygel-simple-media-engine.vala | 51 +++++++
.../rygel-media-export-media-cache.vala | 17 +++-
16 files changed, 325 insertions(+), 85 deletions(-)
---
diff --git a/src/librygel-server/rygel-audio-item.vala b/src/librygel-server/rygel-audio-item.vala
index b6cd7ba..ec9b4c7 100644
--- a/src/librygel-server/rygel-audio-item.vala
+++ b/src/librygel-server/rygel-audio-item.vala
@@ -112,13 +112,8 @@ public class Rygel.AudioItem : MediaFileItem {
return didl_item;
}
- internal override DIDLLiteResource add_resource
- (DIDLLiteObject didl_object,
- string? uri,
- string protocol,
- string? import_uri = null)
- throws Error {
- var res = base.add_resource (didl_object, uri, protocol, import_uri);
+ internal override MediaResource get_primary_resource () {
+ var res = base.get_primary_resource ();
res.duration = this.duration;
res.bitrate = this.bitrate;
diff --git a/src/librygel-server/rygel-http-get-handler.vala b/src/librygel-server/rygel-http-get-handler.vala
index 48638ec..2d88881 100644
--- a/src/librygel-server/rygel-http-get-handler.vala
+++ b/src/librygel-server/rygel-http-get-handler.vala
@@ -71,8 +71,13 @@ internal abstract class Rygel.HTTPGetHandler: GLib.Object {
request.msg.response_headers.append ("Connection", "close");
}
+ /**
+ * Returns the resource size or -1 if not known.
+ */
+ public abstract int64 get_resource_size ();
+
public virtual bool knows_size (HTTPGet request) {
- return false;
+ return this.get_resource_size () >= 0;
}
// Create an HTTPResponse object that will render the body.
diff --git a/src/librygel-server/rygel-http-get.vala b/src/librygel-server/rygel-http-get.vala
index ede92a9..3a4e229 100644
--- a/src/librygel-server/rygel-http-get.vala
+++ b/src/librygel-server/rygel-http-get.vala
@@ -169,6 +169,28 @@ internal class Rygel.HTTPGet : HTTPRequest {
// Add headers
this.handler.add_response_headers (this);
+ int64 response_size;
+ {
+ // Response size might have already been set by one of the response elements
+ response_size = this.msg.response_headers.get_content_length ();
+
+ if (response_size > 0) {
+ this.msg.response_headers.set_content_length (response_size);
+ debug ("Response size set via response element: size "
+ + response_size.to_string());
+ } else {
+ // If not already set by a response element, try to set it to the resource size
+ if ((response_size = this.handler.get_resource_size ()) > 0) {
+ this.msg.response_headers.set_content_length (response_size);
+ debug ("Response size set via response element: size "
+ + response_size.to_string());
+ } else {
+ debug ("Response size unknown");
+ }
+ }
+ // size will factor into other logic below...
+ }
+
// Add general headers
if (this.msg.request_headers.get_one ("Range") != null) {
this.msg.set_status (Soup.Status.PARTIAL_CONTENT);
diff --git a/src/librygel-server/rygel-http-identity-handler.vala
b/src/librygel-server/rygel-http-identity-handler.vala
index 54f03c0..f39e361 100644
--- a/src/librygel-server/rygel-http-identity-handler.vala
+++ b/src/librygel-server/rygel-http-identity-handler.vala
@@ -68,6 +68,10 @@ internal class Rygel.HTTPIdentityHandler : Rygel.HTTPGetHandler {
}
}
+ public override int64 get_resource_size () {
+ return -1;
+ }
+
public override bool knows_size (HTTPGet request) {
var size = this.get_size (request);
diff --git a/src/librygel-server/rygel-http-resource-handler.vala
b/src/librygel-server/rygel-http-resource-handler.vala
index 4dcf575..b1ed7ab 100644
--- a/src/librygel-server/rygel-http-resource-handler.vala
+++ b/src/librygel-server/rygel-http-resource-handler.vala
@@ -80,6 +80,10 @@ internal class Rygel.HTTPMediaResourceHandler : HTTPGetHandler {
}
}
+ public override int64 get_resource_size () {
+ return media_resource.size;
+ }
+
protected override DIDLLiteResource add_resource (DIDLLiteObject didl_object,
HTTPGet request)
throws Error {
diff --git a/src/librygel-server/rygel-http-server.vala b/src/librygel-server/rygel-http-server.vala
index daabf0b..7f60b0f 100644
--- a/src/librygel-server/rygel-http-server.vala
+++ b/src/librygel-server/rygel-http-server.vala
@@ -77,36 +77,10 @@ public class Rygel.HTTPServer : Rygel.TranscodeManager, Rygel.StateMachine {
// This server supports all DLNA delivery modes - so leave those flags alone
}
- internal void add_proxy_resource (DIDLLiteItem didl_item,
- MediaFileItem item)
- throws Error {
- if (this.http_uri_present (item)) {
- return;
- }
-
- var uri = this.create_uri_for_object (item, -1, -1, null, null);
-
- item.add_resource (didl_item, uri, this.get_protocol (), uri);
- }
-
public bool need_proxy (string uri) {
return Uri.parse_scheme (uri) != "http";
}
- private bool http_uri_present (MediaItem item) {
- bool present = false;
-
- foreach (var uri in item.get_uris ()) {
- if (!this.need_proxy (uri)) {
- present = true;
-
- break;
- }
- }
-
- return present;
- }
-
private void on_cancelled (Cancellable cancellable) {
// Cancel all state machines
this.cancellable.cancel ();
diff --git a/src/librygel-server/rygel-http-transcode-handler.vala
b/src/librygel-server/rygel-http-transcode-handler.vala
index f6486e4..94492da 100644
--- a/src/librygel-server/rygel-http-transcode-handler.vala
+++ b/src/librygel-server/rygel-http-transcode-handler.vala
@@ -67,6 +67,10 @@ internal class Rygel.HTTPTranscodeHandler : HTTPGetHandler {
}
}
+ public override int64 get_resource_size () {
+ return -1;
+ }
+
protected override DIDLLiteResource add_resource
(DIDLLiteObject didl_object,
HTTPGet request)
diff --git a/src/librygel-server/rygel-image-item.vala b/src/librygel-server/rygel-image-item.vala
index 219dac3..d81aca4 100644
--- a/src/librygel-server/rygel-image-item.vala
+++ b/src/librygel-server/rygel-image-item.vala
@@ -87,6 +87,16 @@ public class Rygel.ImageItem : MediaFileItem, VisualItem {
this.add_thumbnail_for_uri (uri);
}
+ internal override MediaResource get_primary_resource () {
+ var res = base.get_primary_resource ();
+
+ this.set_visual_resource_properties (res);
+
+ res.dlna_flags |= DLNAFlags.INTERACTIVE_TRANSFER_MODE;
+
+ return res;
+ }
+
internal override void add_resources (DIDLLiteItem didl_item,
bool allow_internal)
throws Error {
@@ -95,19 +105,6 @@ public class Rygel.ImageItem : MediaFileItem, VisualItem {
this.add_thumbnail_resources (didl_item, allow_internal);
}
- internal override DIDLLiteResource add_resource
- (DIDLLiteObject didl_object,
- string? uri,
- string protocol,
- string? import_uri = null)
- throws Error {
- var res = base.add_resource (didl_object, uri, protocol, import_uri);
-
- this.add_visual_props (res);
-
- return res;
- }
-
internal override void add_proxy_resources (HTTPServer server,
DIDLLiteItem didl_item)
throws Error {
diff --git a/src/librygel-server/rygel-media-engine.vala b/src/librygel-server/rygel-media-engine.vala
index 247475b..2314229 100644
--- a/src/librygel-server/rygel-media-engine.vala
+++ b/src/librygel-server/rygel-media-engine.vala
@@ -99,6 +99,29 @@ public abstract class Rygel.MediaEngine : GLib.Object {
public abstract unowned List<DLNAProfile> get_dlna_profiles ();
/**
+ * Retrieve engine-provided resources for the given MediaObject
+ *
+ * The MediaResources returned may include formats/profiles that do not
+ * match the source content byte-for-byte (e.g. transcodes, encrypted
+ * formats, etc). The MediaEngine must return a MediaResource for the raw
+ * MediaObject content if it can support streaming the content directly.
+ *
+ * The order of MediaResources in the returned List should be from
+ * most-preferred to least-preferred and each must have a unique
+ * alphanumeric "name" field.
+ *
+ * Note: The engine should set all delivery-related flags assuming all
+ * delivery forms are supported (e.g. the protocol fields and delivery
+ * flags of the ProtocolInfo). And the resource uri should be set to the
+ * empty string for http-delivered resources. The effective delivery
+ * options and uri will be established by the HTTP server.
+ *
+ * @return A list of #MediaResources<!-- -->s or null if no representations
+ * are provided by the engine for the item.
+ */
+ public abstract async Gee.List<MediaResource> ? get_resources_for_item (MediaObject item);
+
+ /**
* Get a list of the transcoders that are provided by this media engine.
*
* @return A list of #RygelTranscoder<!-- -->s or null if not supported.
diff --git a/src/librygel-server/rygel-media-file-item.vala b/src/librygel-server/rygel-media-file-item.vala
index 249d285..d915c5a 100644
--- a/src/librygel-server/rygel-media-file-item.vala
+++ b/src/librygel-server/rygel-media-file-item.vala
@@ -113,6 +113,8 @@ public abstract class Rygel.MediaFileItem : MediaItem {
upnp_class : upnp_class);
}
+ public static Gee.HashMap<string, string> mime_to_ext;
+
static construct {
try {
address_regex = new Regex (Regex.escape_string ("@ADDRESS@"));
@@ -203,42 +205,116 @@ public abstract class Rygel.MediaFileItem : MediaItem {
internal override DIDLLiteObject? serialize (Serializer serializer,
HTTPServer http_server)
throws Error {
- var didl_item = base.serialize (serializer, http_server)
- as DIDLLiteItem;
-
- /* We list proxy/transcoding resources first instead of original URIs
- * because some crappy MediaRenderer/ControlPoint implemenation out
- * there just choose the first one in the list instead of the one they
- * can handle.
- */
- this.add_proxy_resources (http_server, didl_item);
+ var didl_item = base.serialize (serializer, http_server) as DIDLLiteItem;
if (!this.place_holder) {
- var host_ip = http_server.context.host_ip;
-
- // then original URIs
- bool internal_allowed;
- internal_allowed = http_server.context.interface == "lo" ||
- host_ip == "127.0.0.1";
- this.add_resources (didl_item, internal_allowed);
-
- foreach (var res in didl_item.get_resources ()) {
- res.uri = MediaFileItem.address_regex.replace_literal (res.uri,
- -1,
- 0,
- host_ip);
- }
+ // Subclasses can override add_resources and augment the media resource list (which
+ // should contain the primary resource representations for the MediaItem
+ // at this point) with any secondary representations or alternate delivery
+ // mechanisms they can provide
+ this.add_additional_resources (http_server);
+
+ this.add_proxy_resources (http_server, didl_item);
+
+ this.serialize_resource_list (didl_item, http_server);
}
return didl_item;
}
+ /**
+ * Subclasses override this method to create the type-specific primary MediaResource.
+ *
+ * The resource returned is presumed to represent the "internal" file resource and
+ * a uri referring to the source file. Transport-specific variants can be created
+ * by the caller.
+ */
+ public virtual MediaResource get_primary_resource () {
+ var res = new MediaResource ("primary");
+
+ res.mime_type = this.mime_type;
+ res.dlna_profile = this.dlna_profile;
+ res.dlna_flags = DLNAFlags.BACKGROUND_TRANSFER_MODE;
+
+ // MediaFileItems refer directly to the source URI
+ res.uri = this.get_primary_uri ();
+ try {
+ res.protocol = this.get_protocol_for_uri (res.uri);
+ } catch (Error e) {
+ warning ("Could not determine protocol for " + res.uri);
+ }
+ res.extension = get_extension ();
+ res.size = this.size;
+ return res;
+ }
+
+ /**
+ * Return the file/uri extension that best represents the item's primary resource.
+ */
+ public virtual string get_extension () {
+ string uri_extension = null;
+ // Use the extension from the source content filename, if it has an extension
+ string basename = Path.get_basename (this.get_primary_uri ());
+ int dot_index = -1;
+ if (basename != null) {
+ dot_index = basename.last_index_of (".");
+ if (dot_index > -1) {
+ uri_extension = basename.substring (dot_index + 1);
+ }
+ }
+
+ if (uri_extension == null) {
+ uri_extension = ext_from_mime_type (this.mime_type);
+ }
+ return uri_extension;
+ }
+
+ protected string ext_from_mime_type (string mime_type) {
+ if (mime_to_ext == null) {
+ // Lazy initialization of the static hashmap
+ mime_to_ext = new Gee.HashMap<string, string> ();
+ // videos
+ string[] videos = {"mpeg", "webm", "ogg", "mp4"};
+
+ foreach (string video in videos) {
+ mime_to_ext.set ("video/" + video, video);
+ }
+ mime_to_ext.set ("video/x-matroska", "mkv");
+
+ // audios
+ mime_to_ext.set ("audio/x-wav", "wav");
+ mime_to_ext.set ("audio/x-matroska", "mka");
+ mime_to_ext.set ("audio/L16","pcm");
+ mime_to_ext.set ("audio/vnd.dlna.adts","adts");
+ mime_to_ext.set ("audio/mpeg","mp3");
+ mime_to_ext.set ("audio/3gpp","3gp");
+
+ // images
+ string[] images = {"jpeg", "png"};
+
+ foreach (string image in images) {
+ mime_to_ext.set ("image/" + image, image);
+ }
+
+ // texts
+ mime_to_ext.set ("text/srt", "srt");
+ mime_to_ext.set ("text/xml", "xml");
+
+ // applications? (can be either video or audio?);
+ mime_to_ext.set ("application/ogg", "ogg");
+ }
+
+ if (MediaFileItem.mime_to_ext.has_key (mime_type)) {
+ return mime_to_ext.get (mime_type);
+ }
+
+ return "";
+ }
+
+
internal virtual void add_proxy_resources (HTTPServer server,
DIDLLiteItem didl_item)
throws Error {
- // Proxy resource for the original resources
- server.add_proxy_resource (didl_item, this);
-
if (!this.place_holder) {
// Transcoding resources
server.add_resources (didl_item, this);
@@ -274,4 +350,18 @@ public abstract class Rygel.MediaFileItem : MediaItem {
}
}
}
+
+ /**
+ * Subclasses can override this method to augment the MediaObject MediaResource
+ * list with secondary MediaResource objects representing derivative resources.
+ *
+ * Note: Implementations should add both internal/file-based resources and HTTP-accessible
+ * resources to the MediaResource list.
+ * FIXME: Will be renamed once we can safely remove old add_resources
+ */
+ internal virtual void add_additional_resources (HTTPServer server) {
+ /* Do nothing - provide default implementation to avoid unnecessary
+ empty code blocks.
+ */
+ }
}
diff --git a/src/librygel-server/rygel-media-object.vala b/src/librygel-server/rygel-media-object.vala
index 179ee06..e05d467 100644
--- a/src/librygel-server/rygel-media-object.vala
+++ b/src/librygel-server/rygel-media-object.vala
@@ -266,7 +266,7 @@ public abstract class Rygel.MediaObject : GLib.Object {
} else {
try {
var protocol = this.get_protocol_for_uri (res.uri);
- if (protocol != "internal") {
+ if (protocol != "internal" || http_server.is_local ()) {
// Exclude internal resources when request is non-local
var didl_resource = didl_object.add_resource ();
res.serialize (didl_resource);
diff --git a/src/librygel-server/rygel-video-item.vala b/src/librygel-server/rygel-video-item.vala
index c9d3a16..6dc44a4 100644
--- a/src/librygel-server/rygel-video-item.vala
+++ b/src/librygel-server/rygel-video-item.vala
@@ -117,15 +117,10 @@ public class Rygel.VideoItem : AudioItem, VisualItem {
this.add_thumbnail_resources (didl_item, allow_internal);
}
- internal override DIDLLiteResource add_resource
- (DIDLLiteObject didl_object,
- string? uri,
- string protocol,
- string? import_uri = null)
- throws Error {
- var res = base.add_resource (didl_object, uri, protocol, import_uri);
-
- this.add_visual_props (res);
+ internal override MediaResource get_primary_resource () {
+ var res = base.get_primary_resource ();
+
+ this.set_visual_resource_properties (res);
return res;
}
diff --git a/src/librygel-server/rygel-visual-item.vala b/src/librygel-server/rygel-visual-item.vala
index 2ab8897..85068e5 100644
--- a/src/librygel-server/rygel-visual-item.vala
+++ b/src/librygel-server/rygel-visual-item.vala
@@ -83,7 +83,7 @@ public interface Rygel.VisualItem : MediaFileItem {
}
}
- internal void add_visual_props (DIDLLiteResource res) {
+ internal void set_visual_resource_properties (MediaResource res) {
res.width = this.width;
res.height = this.height;
res.color_depth = this.color_depth;
diff --git a/src/media-engines/gstreamer/rygel-gst-media-engine.vala
b/src/media-engines/gstreamer/rygel-gst-media-engine.vala
index 1486737..f2b4f81 100644
--- a/src/media-engines/gstreamer/rygel-gst-media-engine.vala
+++ b/src/media-engines/gstreamer/rygel-gst-media-engine.vala
@@ -22,6 +22,7 @@
using Gst;
using Gee;
+using GUPnP;
// Remove for GStreamer 1.0
[CCode (cname = "PRESET_DIR")]
@@ -128,6 +129,66 @@ public class Rygel.GstMediaEngine : Rygel.MediaEngine {
}
}
+ public override async Gee.List<MediaResource>? get_resources_for_item (MediaObject
+ object) {
+ if (! (object is MediaFileItem)) {
+ warning ("Can only process file-based MediaObjects (MediaFileItems)");
+ return null;
+ }
+
+ var item = object as MediaFileItem;
+
+ // For MediaFileItems, the primary URI refers directly to the content
+ string source_uri = item.get_primary_uri ();
+
+ debug ("get_resources_for_item(%s)", source_uri);
+
+ if (!source_uri.has_prefix ("file://")) {
+ warning ("Can't process non-file uri " + source_uri);
+ return null;
+ }
+
+ Gee.List<MediaResource> resources = new Gee.ArrayList<MediaResource> ();
+
+ var primary_res = item.get_primary_resource ();
+
+ // The GstMediaEngine only supports byte-based seek on the primary resource currently
+ primary_res.dlna_operation = DLNAOperation.RANGE;
+
+ // The GstMediaEngine supports connection stalling on the primary resource
+ primary_res.dlna_flags |= DLNAFlags.CONNECTION_STALL;
+
+ // Add a resource for http consumption
+ MediaResource http_res = new MediaResource.from_resource ("primary_http",
+ primary_res);
+ http_res.uri = ""; // The URI needs to be assigned by the MediaServer
+ resources.add (http_res);
+
+ // Put the primary resource as most-preferred (front of the list)
+ resources.add (primary_res);
+
+ return resources;
+ }
+
+ public override DataSource? create_data_source_for_resource
+ (MediaObject object,
+ MediaResource res) throws Error {
+ if (!(object is MediaFileItem)) {
+ warning ("Can only process file-based MediaObjects (MediaFileItem)");
+ return null;
+ }
+
+ var item = object as MediaFileItem;
+
+ string source_uri = item.get_primary_uri ();
+ debug ("creating data source for %s", source_uri);
+
+ DataSource ds = new GstDataSource (source_uri);
+
+ return ds;
+ }
+
+
public DataSource create_data_source_from_element (Element element) {
return new GstDataSource.from_element (element);
}
diff --git a/src/media-engines/simple/rygel-simple-media-engine.vala
b/src/media-engines/simple/rygel-simple-media-engine.vala
index 512a9f2..42253c6 100644
--- a/src/media-engines/simple/rygel-simple-media-engine.vala
+++ b/src/media-engines/simple/rygel-simple-media-engine.vala
@@ -20,6 +20,8 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+using GUPnP;
+
/**
* The simple media engine does not use GStreamer or any other
* multimedia framework. Therefore its capabilities are limited.
@@ -40,6 +42,55 @@ internal class Rygel.SimpleMediaEngine : MediaEngine {
return null;
}
+ public override async Gee.List<MediaResource>? get_resources_for_item (MediaObject
+ object) {
+ if (! (object is MediaFileItem)) {
+ warning ("Can only process file-based MediaObjects (MediaFileItems)");
+ return null;
+ }
+
+ var item = object as MediaFileItem;
+
+ // For MediaFileItems, uri 0 is the file URI referring directly to the content
+ string source_uri = item.get_primary_uri ();
+ if (!source_uri.has_prefix ("file://")) {
+ warning ("Can't process non-file uri " + source_uri);
+ }
+
+ debug ("get_resources_for_item (%s)", source_uri);
+
+ Gee.List<MediaResource> resources = new Gee.ArrayList<MediaResource> ();
+
+ var primary_res = item.get_primary_resource ();
+
+ // The SimpleMediaEngine supports only byte-based seek
+ primary_res.dlna_operation = GUPnP.DLNAOperation.RANGE;
+
+ // The SimpleMediaEngine supports connection stalling on
+ primary_res.dlna_flags |= DLNAFlags.CONNECTION_STALL;
+
+ // Add a resource for http consumption (as SimpleMediaEngine can handle http)
+ MediaResource http_res = new MediaResource.from_resource ("primary_http",
+ primary_res);
+ http_res.uri = ""; // The URI needs to be assigned by the MediaServer
+ resources.add (http_res);
+
+ resources.add (primary_res);
+
+ return resources;
+ }
+
+ public override DataSource? create_data_source_for_resource
+ (MediaObject object,
+ MediaResource res) throws Error {
+ if (!(object is MediaFileItem)) {
+ warning ("Can only process file-based MediaObjects (MediaFileItem)");
+ return null;
+ }
+
+ string source_uri = object.get_primary_uri ();
+ return new SimpleDataSource (source_uri);
+ }
public override DataSource? create_data_source (string uri) {
if (!uri.has_prefix ("file://")) {
return null;
diff --git a/src/plugins/media-export/rygel-media-export-media-cache.vala
b/src/plugins/media-export/rygel-media-export-media-cache.vala
index d24c81b..c4d95d9 100644
--- a/src/plugins/media-export/rygel-media-export-media-cache.vala
+++ b/src/plugins/media-export/rygel-media-export-media-cache.vala
@@ -910,7 +910,22 @@ public class Rygel.MediaExport.MediaCache : Object {
if (uri != null) {
item.add_uri (uri);
}
- break;
+
+ // Call the MediaEngine to determine which item representations it can support
+ var media_engine = MediaEngine.get_default ( );
+ media_engine.get_resources_for_item.begin ( item,
+ (obj, res) => {
+ var added_resources = media_engine
+ .get_resources_for_item.end (res);
+ debug ("Adding %d resources to item source %s",
+ added_resources.size, item.get_primary_uri ());
+ foreach (var resrc in added_resources) {
+ debug ("Media-export item media resource %s",
+ resrc.get_name ());
+ }
+ item.get_resource_list ().add_all (added_resources);
+ });
+ break;
default:
assert_not_reached ();
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]