diff --git a/modules/gmedia/config/optional/views.view.group_media.yml b/modules/gmedia/config/optional/views.view.group_media.yml
new file mode 100644
index 0000000..0c4fd10
--- /dev/null
+++ b/modules/gmedia/config/optional/views.view.group_media.yml
@@ -0,0 +1,841 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - group
+    - media
+id: group_media
+label: 'Group media'
+module: views
+description: ''
+tag: ''
+base_table: media_field_data
+base_field: mid
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: group_permission
+        options:
+          group_permission: 'access group_media overview'
+      cache:
+        type: tag
+        options: {  }
+      query:
+        type: views_query
+        options:
+          disable_sql_rewrite: false
+          distinct: false
+          replica: false
+          query_comment: ''
+          query_tags: {  }
+      exposed_form:
+        type: basic
+        options:
+          submit_button: Apply
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+      pager:
+        type: mini
+        options:
+          items_per_page: 50
+          offset: 0
+          id: 0
+          total_pages: null
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+          tags:
+            previous: ‹‹
+            next: ››
+      style:
+        type: table
+      row:
+        type: fields
+      fields:
+        name:
+          id: name
+          table: media_field_data
+          field: name
+          entity_type: media
+          entity_field: media
+          alter:
+            alter_text: false
+            make_link: false
+            absolute: false
+            trim: false
+            word_boundary: false
+            ellipsis: false
+            strip_tags: false
+            html: false
+          hide_empty: false
+          empty_zero: false
+          settings:
+            link_to_entity: true
+          plugin_id: field
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Name
+          exclude: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_alter_empty: true
+          click_sort_column: value
+          type: string
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+        bundle:
+          id: bundle
+          table: media_field_data
+          field: bundle
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Media type'
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: target_id
+          type: entity_reference_label
+          settings:
+            link: false
+          group_column: target_id
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: media
+          entity_field: bundle
+          plugin_id: field
+        status:
+          id: status
+          table: media_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Status
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: boolean
+          settings:
+            format: custom
+            format_custom_true: Published
+            format_custom_false: Unpublished
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: media
+          entity_field: status
+          plugin_id: field
+        changed:
+          id: changed
+          table: media_field_data
+          field: changed
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Updated
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          click_sort_column: value
+          type: timestamp
+          settings:
+            date_format: medium
+            custom_date_format: ''
+            timezone: ''
+          group_column: value
+          group_columns: {  }
+          group_rows: true
+          delta_limit: 0
+          delta_offset: 0
+          delta_reversed: false
+          delta_first_last: false
+          multi_type: separator
+          separator: ', '
+          field_api_classes: false
+          entity_type: media
+          entity_field: changed
+          plugin_id: field
+        view_group_content:
+          id: view_group_content
+          table: group_content
+          field: view_group_content
+          relationship: group_content
+          group_type: group
+          admin_label: ''
+          label: 'Link to Group content'
+          exclude: true
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: 'View relation'
+          output_url_as_text: false
+          absolute: false
+          entity_type: group_content
+          plugin_id: entity_link
+        edit_group_content:
+          id: edit_group_content
+          table: group_content
+          field: edit_group_content
+          relationship: group_content
+          group_type: group
+          admin_label: ''
+          label: 'Link to edit Group content'
+          exclude: true
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: 'Edit relation'
+          output_url_as_text: false
+          absolute: false
+          entity_type: group_content
+          plugin_id: entity_link_edit
+        delete_group_content:
+          id: delete_group_content
+          table: group_content
+          field: delete_group_content
+          relationship: group_content
+          group_type: group
+          admin_label: ''
+          label: 'Link to delete Group content'
+          exclude: true
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: 'Delete relation'
+          output_url_as_text: false
+          absolute: false
+          entity_type: group_content
+          plugin_id: entity_link_delete
+        edit_media:
+          id: edit_media
+          table: media
+          field: edit_media
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Link to edit Media'
+          exclude: true
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: 'Edit media'
+          output_url_as_text: false
+          absolute: false
+          entity_type: media
+          plugin_id: entity_link_edit
+        delete_media:
+          id: delete_media
+          table: media
+          field: delete_media
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: 'Link to delete Media'
+          exclude: true
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          text: 'Delete node'
+          output_url_as_text: false
+          absolute: false
+          entity_type: media
+          plugin_id: entity_link_delete
+        dropbutton:
+          id: dropbutton
+          table: views
+          field: dropbutton
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: Operations
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: true
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          fields:
+            view_group_content: view_group_content
+            edit_group_content: edit_group_content
+            delete_group_content: delete_group_content
+            edit_media: edit_media
+            delete_media: delete_media
+            name: '0'
+            bundle: '0'
+            status: '0'
+            changed: '0'
+          destination: true
+          plugin_id: dropbutton
+      filters:
+        status:
+          id: status
+          table: media_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: '1'
+          group: 1
+          exposed: true
+          expose:
+            operator_id: ''
+            label: Published
+            description: null
+            use_operator: false
+            operator: status_op
+            identifier: status
+            required: true
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+          is_grouped: true
+          group_info:
+            label: 'Published status'
+            description: ''
+            identifier: status
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items:
+              1:
+                title: Published
+                operator: '='
+                value: '1'
+              2:
+                title: Unpublished
+                operator: '='
+                value: '0'
+          plugin_id: boolean
+          entity_type: media
+          entity_field: status
+        bundle:
+          id: bundle
+          table: media_field_data
+          field: bundle
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: bundle_op
+            label: Type
+            description: ''
+            use_operator: false
+            operator: bundle_op
+            identifier: bundle
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            reduce: false
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: media
+          entity_field: bundle
+          plugin_id: bundle
+      sorts: {  }
+      title: 'Group media'
+      header: {  }
+      footer: {  }
+      empty:
+        area_text_custom:
+          id: area_text_custom
+          table: views
+          field: area_text_custom
+          relationship: none
+          group_type: group
+          admin_label: ''
+          empty: true
+          tokenize: false
+          content: 'No content available.'
+          plugin_id: text_custom
+      relationships:
+        group_content:
+          id: group_content
+          table: media_field_data
+          field: group_content
+          relationship: none
+          group_type: group
+          admin_label: 'Content relation'
+          required: true
+          group_content_plugins:
+            'group_media:audio': '0'
+            'group_media:file': '0'
+            'group_media:image': '0'
+            'group_media:remote_video': '0'
+            'group_media:video': '0'
+          entity_type: media
+          plugin_id: group_content_to_entity_reverse
+      arguments:
+        gid:
+          id: gid
+          table: group_content_field_data
+          field: gid
+          relationship: group_content
+          group_type: group
+          admin_label: ''
+          default_action: 'access denied'
+          exception:
+            value: all
+            title_enable: false
+            title: All
+          title_enable: true
+          title: '{{ arguments.gid|placeholder }} media'
+          default_argument_type: fixed
+          default_argument_options:
+            argument: ''
+          default_argument_skip_url: false
+          summary_options:
+            base_path: ''
+            count: true
+            items_per_page: 25
+            override: false
+          summary:
+            sort_order: asc
+            number_of_records: 0
+            format: default_summary
+          specify_validation: true
+          validate:
+            type: 'entity:group'
+            fail: 'not found'
+          validate_options:
+            operation: view
+            multiple: 0
+            bundles: {  }
+            access: false
+          break_phrase: false
+          not: false
+          entity_type: group_content
+          entity_field: gid
+          plugin_id: group_id
+      display_extenders: {  }
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - route.group
+        - url
+        - url.query_args
+        - user.group_permissions
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: group/%group/media
+      menu:
+        type: tab
+        title: Media
+        description: ''
+        expanded: false
+        parent: ''
+        weight: 30
+        context: '0'
+        menu_name: main
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - route.group
+        - url
+        - url.query_args
+        - user.group_permissions
+      tags: {  }
diff --git a/modules/gmedia/gmedia.group.permissions.yml b/modules/gmedia/gmedia.group.permissions.yml
new file mode 100644
index 0000000..dd6921f
--- /dev/null
+++ b/modules/gmedia/gmedia.group.permissions.yml
@@ -0,0 +1,3 @@
+access group_media overview:
+  title: 'Access group media overview'
+  description: 'Access the overview of all group media items, regardless of bundle'
diff --git a/modules/gmedia/gmedia.info.yml b/modules/gmedia/gmedia.info.yml
new file mode 100644
index 0000000..487cd50
--- /dev/null
+++ b/modules/gmedia/gmedia.info.yml
@@ -0,0 +1,9 @@
+name: 'Group Media'
+description: 'Enables Group functionality for media items'
+package: 'Group'
+type: 'module'
+version: 1.0
+core_version_requirement: ^8.8 || ^9
+dependencies:
+  - 'media'
+  - 'group'
diff --git a/modules/gmedia/gmedia.links.action.yml b/modules/gmedia/gmedia.links.action.yml
new file mode 100644
index 0000000..d200a22
--- /dev/null
+++ b/modules/gmedia/gmedia.links.action.yml
@@ -0,0 +1,11 @@
+group_content.group_media_add_page:
+  route_name: 'entity.group_content.group_media_add_page'
+  title: 'Add media item to group'
+  appears_on:
+    - 'view.group_media.page_1'
+
+group_content.group_media_relate_page:
+  route_name: 'entity.group_content.group_media_relate_page'
+  title: 'Relate media item to group'
+  appears_on:
+    - 'view.group_media.page_1'
diff --git a/modules/gmedia/gmedia.module b/modules/gmedia/gmedia.module
new file mode 100644
index 0000000..556a27a
--- /dev/null
+++ b/modules/gmedia/gmedia.module
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Enables Group functionality for the Media module.
+ */
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Access\AccessResult;
+use Drupal\group\Entity\GroupContentType;
+use Drupal\media\MediaTypeInterface;
+
+/**
+ * Implements hook_ENTITY_TYPE_insert().
+ */
+function gmedia_media_type_insert(MediaTypeInterface $mediaType) {
+  \Drupal::service('plugin.manager.group_content_enabler')->clearCachedDefinitions();
+}
+
+/**
+ * Implements hook_media_access().
+ *
+ * When trying to view, update or delete a media item it suffices to have the
+ * right to do so in only one group the media item belongs to. If you wish
+ * to prevent any such action on your own terms, implement hook_media_access()
+ * in your module.
+ */
+function gmedia_media_access(EntityInterface $media, $op, AccountInterface $account) {
+  // We do not care about create access as we have our own wizard for that. Any
+  // operation aside from 'view', 'update' and 'delete' is also unsupported.
+  if (!in_array($op, ['view', 'update', 'delete']) || !$media->id()) {
+    return AccessResult::neutral();
+  }
+
+  $plugin_id = 'group_media:' . $media->bundle();
+
+  // Only act if there are group content types for this media type.
+  $group_content_types = GroupContentType::loadByContentPluginId($plugin_id);
+  if (empty($group_content_types)) {
+    return AccessResult::neutral();
+  }
+
+  // Load all the group content for this media item.
+  $group_contents = \Drupal::entityTypeManager()
+    ->getStorage('group_content')
+    ->loadByProperties([
+      'type' => array_keys($group_content_types),
+      'entity_id' => $media->id(),
+    ]);
+
+  // If the media item does not belong to any group, we have nothing to say.
+  if (empty($group_contents)) {
+    return AccessResult::neutral();
+  }
+
+  /** @var \Drupal\group\Entity\GroupInterface[] $groups */
+  $groups = [];
+  foreach ($group_contents as $group_content) {
+    /** @var \Drupal\group\Entity\GroupContentInterface $group_content */
+    $group = $group_content->getGroup();
+    $groups[$group->id()] = $group;
+  }
+
+  // From this point on you need group to allow you to perform the requested
+  // operation. If you are not granted access for a media item belonging to
+  // a group, you should be denied access instead.
+  switch ($op) {
+    case 'view':
+      $is_published = $media->isPublished();
+
+      foreach ($groups as $group) {
+        if ((!$is_published && $group->hasPermission("view unpublished $plugin_id entity", $account)) ||
+          ($is_published && $group->hasPermission("view $plugin_id entity", $account))) {
+          return AccessResult::allowed();
+        }
+      }
+      break;
+
+    case 'update':
+    case 'delete':
+      foreach ($groups as $group) {
+        if ($group->hasPermission( "$op any $plugin_id entity", $account)) {
+          return AccessResult::allowed();
+        }
+        elseif ($account->id() == $media->getOwnerId() && $group->hasPermission("$op own $plugin_id entity", $account)) {
+          return AccessResult::allowed();
+        }
+      }
+      break;
+  }
+
+  return AccessResult::forbidden();
+}
diff --git a/modules/gmedia/gmedia.routing.yml b/modules/gmedia/gmedia.routing.yml
new file mode 100644
index 0000000..47f500e
--- /dev/null
+++ b/modules/gmedia/gmedia.routing.yml
@@ -0,0 +1,2 @@
+route_callbacks:
+  - '\Drupal\gmedia\Routing\GroupMediaRouteProvider::getRoutes'
diff --git a/modules/gmedia/gmedia.services.yml b/modules/gmedia/gmedia.services.yml
new file mode 100644
index 0000000..d839a91
--- /dev/null
+++ b/modules/gmedia/gmedia.services.yml
@@ -0,0 +1,5 @@
+services:
+  access_check.gmedia.admin:
+    class: Drupal\gmedia\Access\GroupMediaAdminAccessCheck
+    tags:
+      - { name: access_check, applies_to: _group_media_admin_access }
diff --git a/modules/gmedia/src/Access/GroupMediaAdminAccessCheck.php b/modules/gmedia/src/Access/GroupMediaAdminAccessCheck.php
new file mode 100644
index 0000000..fb55c32
--- /dev/null
+++ b/modules/gmedia/src/Access/GroupMediaAdminAccessCheck.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace Drupal\gmedia\Access;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\group\Entity\GroupInterface;
+use Drupal\media\MediaInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Determines access to group media forms.
+ */
+class GroupMediaAdminAccessCheck implements AccessInterface {
+
+  /**
+   * Checks access to the group media pages.
+   *
+   * @param \Symfony\Component\Routing\Route $route
+   *   The route to check against.
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The currently logged in account.
+   * @param \Drupal\group\Entity\GroupInterface $group
+   *   The group with which to relate the item.
+   * @param \Drupal\media\MediaInterface $media_type
+   *   The media type in which to create the item.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access(Route $route, AccountInterface $account, GroupInterface $group, MediaInterface $media_type) {
+    $needs_access = $route->getRequirement('_group_media_admin_access') === 'TRUE';
+
+    // We can only get the group content type ID if the plugin is installed.
+    if (!$group->getGroupType()->hasContentPlugin("group_media:$media_type")) {
+      return AccessResult::neutral();
+    }
+
+    // Determine whether the user can manipulate items of the provided type.
+    $access = FALSE;
+    foreach (['create', 'edit', 'delete'] as $op) {
+      $permission = "$op $media_type item";
+      $access = $access || $group->hasPermission($permission, $account);
+    }
+
+    // Only allow access if the user can manipulate group items of the provided
+    // type or if he doesn't need access to do so.
+    return AccessResult::allowedIf($access xor !$needs_access);
+  }
+
+}
diff --git a/modules/gmedia/src/Controller/GroupMediaController.php b/modules/gmedia/src/Controller/GroupMediaController.php
new file mode 100644
index 0000000..537579a
--- /dev/null
+++ b/modules/gmedia/src/Controller/GroupMediaController.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Drupal\gmedia\Controller;
+
+use Drupal\Core\Entity\EntityFormBuilderInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\group\Entity\Controller\GroupContentController;
+use Drupal\group\Entity\GroupInterface;
+use Drupal\group\Plugin\GroupContentEnablerManagerInterface;
+use Drupal\user\PrivateTempStoreFactory;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Returns responses for 'group_media' GroupContent routes.
+ */
+class GroupMediaController extends GroupContentController {
+
+  /**
+   * The group content plugin manager.
+   *
+   * @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface
+   */
+  protected $pluginManager;
+
+  /**
+   * Constructs a new GroupMediaController.
+   *
+   * @param \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager
+   *   The group content plugin manager.
+   * @param \Drupal\user\PrivateTempStoreFactory $temp_store_factory
+   *   The private store factory.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
+   *   The entity form builder.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(GroupContentEnablerManagerInterface $plugin_manager, PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, RendererInterface $renderer) {
+    parent::__construct($temp_store_factory, $entity_type_manager, $entity_form_builder, $renderer);
+    $this->pluginManager = $plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.group_content_enabler'),
+      $container->get('user.private_tempstore'),
+      $container->get('entity_type.manager'),
+      $container->get('entity.form_builder'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addPage(GroupInterface $group, $create_mode = FALSE) {
+    $build = parent::addPage($group, $create_mode);
+
+    // Do not interfere with redirects.
+    if (!is_array($build)) {
+      return $build;
+    }
+
+    // Overwrite the label and description for all of the displayed bundles.
+    $storage_handler = $this->entityTypeManager->getStorage('media_type');
+    foreach ($this->addPageBundles($group, $create_mode) as $plugin_id => $bundle_name) {
+      if (!empty($build['#bundles'][$bundle_name])) {
+        $plugin = $group->getGroupType()->getContentPlugin($plugin_id);
+        $bundle_label = $storage_handler->load($plugin->getEntityBundle())->label();
+
+        $t_args = ['%media_type' => $bundle_label];
+        $description = $create_mode
+          ? $this->t('Create a media of type %media_type in the group.', $t_args)
+          : $this->t('Add an existing media of type %media_type to the group.', $t_args);
+
+        $build['#bundles'][$bundle_name]['label'] = $bundle_label;
+        $build['#bundles'][$bundle_name]['description'] = $description;
+      }
+    }
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function addPageBundles(GroupInterface $group, $create_mode) {
+    $bundles = [];
+
+    // Retrieve all group_media plugins for the group's type.
+    $plugin_ids = $this->pluginManager->getInstalledIds($group->getGroupType());
+    foreach ($plugin_ids as $key => $plugin_id) {
+      if (strpos($plugin_id, 'group_media:') !== 0) {
+        unset($plugin_ids[$key]);
+      }
+    }
+
+    // Retrieve all of the responsible group content types, keyed by plugin ID.
+    $storage = $this->entityTypeManager->getStorage('group_content_type');
+    $properties = ['group_type' => $group->bundle(), 'content_plugin' => $plugin_ids];
+    foreach ($storage->loadByProperties($properties) as $bundle => $group_content_type) {
+      /** @var \Drupal\group\Entity\GroupContentTypeInterface $group_content_type */
+      $bundles[$group_content_type->getContentPluginId()] = $bundle;
+    }
+
+    return $bundles;
+  }
+
+}
diff --git a/modules/gmedia/src/Plugin/GroupConfigEnabler/GroupMediaType.php b/modules/gmedia/src/Plugin/GroupConfigEnabler/GroupMediaType.php
new file mode 100644
index 0000000..b34214a
--- /dev/null
+++ b/modules/gmedia/src/Plugin/GroupConfigEnabler/GroupMediaType.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Drupal\gmedia\Plugin\GroupConfigEnabler;
+
+use Drupal\group\Plugin\GroupConfigEnablerBase;
+
+/**
+ * Provides a content enabler for media items.
+ *
+ * @GroupConfigEnabler(
+ *   id = "group_media_type",
+ *   label = @Translation("Group media type"),
+ *   description = @Translation("Adds media type to groups both publicly and privately."),
+ *   entity_type_id = "media_type",
+ * )
+ */
+class GroupMediaType extends GroupConfigEnablerBase {
+}
diff --git a/modules/gmedia/src/Plugin/GroupContentEnabler/GroupMedia.php b/modules/gmedia/src/Plugin/GroupContentEnabler/GroupMedia.php
new file mode 100644
index 0000000..2ee5463
--- /dev/null
+++ b/modules/gmedia/src/Plugin/GroupContentEnabler/GroupMedia.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\gmedia\Plugin\GroupContentEnabler;
+
+use Drupal\group\Entity\GroupInterface;
+use Drupal\group\Plugin\GroupContentEnablerBase;
+use Drupal\media\Entity\MediaType;
+use Drupal\Core\Url;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Provides a content enabler for medias.
+ *
+ * @GroupContentEnabler(
+ *   id = "group_media",
+ *   label = @Translation("Group media"),
+ *   description = @Translation("Adds medias to groups both publicly and privately."),
+ *   entity_type_id = "media",
+ *   entity_access = TRUE,
+ *   reference_label = @Translation("Title"),
+ *   reference_description = @Translation("The title of the media to add to the group"),
+ *   deriver = "Drupal\gmedia\Plugin\GroupContentEnabler\GroupMediaDeriver"
+ * )
+ */
+class GroupMedia extends GroupContentEnablerBase {
+
+  /**
+   * Retrieves the media type this plugin supports.
+   *
+   * @return \Drupal\media\mediaTypeInterface
+   *   The media type this plugin supports.
+   */
+  protected function getMediaType() {
+    return MediaType::load($this->getEntityBundle());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getGroupOperations(GroupInterface $group) {
+    $account = \Drupal::currentUser();
+    $plugin_id = $this->getPluginId();
+    $type = $this->getEntityBundle();
+    $operations = [];
+
+    if ($group->hasPermission("create $plugin_id entity", $account)) {
+      $route_params = ['group' => $group->id(), 'plugin_id' => $plugin_id];
+      $operations["gmedia-create-$type"] = [
+        'title' => $this->t('Create @type', ['@type' => $this->getMediaType()->label()]),
+        'url' => new Url('entity.group_content.create_form', $route_params),
+        'weight' => 30,
+      ];
+    }
+
+    return $operations;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getTargetEntityPermissions() {
+    $permissions = parent::getTargetEntityPermissions();
+    $plugin_id = $this->getPluginId();
+
+    // Add a 'view unpublished' permission by re-using most of the 'view' one.
+    $original = $permissions["view $plugin_id entity"];
+    $permissions["view unpublished $plugin_id entity"] = [
+      'title' => str_replace('View ', 'View unpublished ', $original['title']),
+    ] + $original;
+
+    return $permissions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    $config = parent::defaultConfiguration();
+    $config['entity_cardinality'] = 1;
+    return $config;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildConfigurationForm($form, $form_state);
+
+    // Disable the entity cardinality field as the functionality of this module
+    // relies on a cardinality of 1. We don't just hide it, though, to keep a UI
+    // that's consistent with other content enabler plugins.
+    $info = $this->t("This field has been disabled by the plugin to guarantee the functionality that's expected of it.");
+    $form['entity_cardinality']['#disabled'] = TRUE;
+    $form['entity_cardinality']['#description'] .= '<br /><em>' . $info . '</em>';
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    $dependencies = parent::calculateDependencies();
+    $dependencies['config'][] = 'media.type.' . $this->getEntityBundle();
+    return $dependencies;
+  }
+
+}
diff --git a/modules/gmedia/src/Plugin/GroupContentEnabler/GroupMediaDeriver.php b/modules/gmedia/src/Plugin/GroupContentEnabler/GroupMediaDeriver.php
new file mode 100644
index 0000000..200e4a3
--- /dev/null
+++ b/modules/gmedia/src/Plugin/GroupContentEnabler/GroupMediaDeriver.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Drupal\gmedia\Plugin\GroupContentEnabler;
+
+use Drupal\media\Entity\MediaType;
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+
+class GroupMediaDeriver extends DeriverBase {
+
+  /**
+   * {@inheritdoc}.
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    foreach (MediaType::loadMultiple() as $name => $media_type) {
+      $label = $media_type->label();
+
+      $this->derivatives[$name] = [
+        'entity_bundle' => $name,
+        'label' => t('Group media (@type)', ['@type' => $label]),
+        'description' => t('Adds %type content to groups both publicly and privately.', ['%type' => $label]),
+      ] + $base_plugin_definition;
+    }
+
+    return $this->derivatives;
+  }
+
+}
diff --git a/modules/gmedia/src/Routing/GroupMediaRouteProvider.php b/modules/gmedia/src/Routing/GroupMediaRouteProvider.php
new file mode 100644
index 0000000..b552e5a
--- /dev/null
+++ b/modules/gmedia/src/Routing/GroupMediaRouteProvider.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Drupal\gmedia\Routing;
+
+use Drupal\media\Entity\MediaType;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for group_media group content.
+ */
+class GroupMediaRouteProvider {
+
+  /**
+   * Provides the shared collection route for group media plugins.
+   */
+  public function getRoutes() {
+    $routes = $plugin_ids = $permissions_add = $permissions_create = [];
+
+    foreach (MediaType::loadMultiple() as $name => $media_type) {
+      $plugin_id = "group_media:$name";
+
+      $plugin_ids[] = $plugin_id;
+      $permissions_add[] = "create $plugin_id content";
+      $permissions_create[] = "create $plugin_id entity";
+    }
+
+    // If there are no media types yet, we cannot have any plugin IDs and should
+    // therefore exit early because we cannot have any routes for them either.
+    if (empty($plugin_ids)) {
+      return $routes;
+    }
+
+    $routes['entity.group_content.group_media_relate_page'] = new Route('group/{group}/media/add');
+    $routes['entity.group_content.group_media_relate_page']
+      ->setDefaults([
+        '_title' => 'Relate media',
+        '_controller' => '\Drupal\gmedia\Controller\GroupMediaController::addPage',
+      ])
+      ->setRequirement('_group_permission', implode('+', $permissions_add))
+      ->setRequirement('_group_installed_content', implode('+', $plugin_ids))
+      ->setOption('_group_operation_route', TRUE);
+
+    $routes['entity.group_content.group_media_add_page'] = new Route('group/{group}/media/create');
+    $routes['entity.group_content.group_media_add_page']
+      ->setDefaults([
+        '_title' => 'Create media',
+        '_controller' => '\Drupal\gmedia\Controller\GroupMediaController::addPage',
+        'create_mode' => TRUE,
+      ])
+      ->setRequirement('_group_permission', implode('+', $permissions_create))
+      ->setRequirement('_group_installed_content', implode('+', $plugin_ids))
+      ->setOption('_group_operation_route', TRUE);
+
+    return $routes;
+  }
+
+}
