diff --git a/app/controllers/api/v1/rooms_controller.rb b/app/controllers/api/v1/rooms_controller.rb
index 8552bdd7456eae2827eeb90c026ba8f408e4003f..b510c1b896102d8c8699562beca39ae64bd4a04b 100644
--- a/app/controllers/api/v1/rooms_controller.rb
+++ b/app/controllers/api/v1/rooms_controller.rb
@@ -19,9 +19,9 @@
 module Api
   module V1
     class RoomsController < ApiController
-      skip_before_action :ensure_authenticated, only: %i[public_show]
+      skip_before_action :ensure_authenticated, only: %i[public_show public_recordings]
 
-      before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show]
+      before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show public_recordings]
 
       before_action only: %i[create index] do
         ensure_authorized('CreateRoom')
@@ -136,6 +136,16 @@ module Api
         render_data data: room_recordings, meta: pagy_metadata(pagy), status: :ok
       end
 
+      # GET /api/v1/rooms/:friendly_id/public_recordings.json
+      # Returns all of a specific room's PUBLIC recordings
+      def public_recordings
+        sort_config = config_sorting(allowed_columns: %w[name length])
+
+        pagy, recordings = pagy(@room.public_recordings.order(sort_config, recorded_at: :desc).public_search(params[:search]))
+
+        render_data data: recordings, meta: pagy_metadata(pagy), serializer: PublicRecordingSerializer, status: :ok
+      end
+
       # GET /api/v1/rooms/:friendly_id/recordings_processing.json
       # Returns the total number of processing recordings for a specific room
       def recordings_processing
diff --git a/app/models/recording.rb b/app/models/recording.rb
index 41bde789850e95a614d9f27838b3471365210261..94578602c2917649fb86149759e0e8fe4b9ca70e 100644
--- a/app/models/recording.rb
+++ b/app/models/recording.rb
@@ -46,4 +46,13 @@ class Recording < ApplicationRecord
 
     all.includes(:formats)
   end
+
+  def self.public_search(input)
+    if input
+      return joins(:formats).where('recordings.name ILIKE :input OR formats.recording_type ILIKE :input',
+                                   input: "%#{input}%").includes(:formats)
+    end
+
+    all.includes(:formats)
+  end
 end
diff --git a/app/models/room.rb b/app/models/room.rb
index edc9d58a76c9c0c8147baca9081d86a4e5ad7f04..649c5c445c54a7721eeae9cecafed02d4e5422a6 100644
--- a/app/models/room.rb
+++ b/app/models/room.rb
@@ -75,6 +75,10 @@ class Room < ApplicationRecord
     end
   end
 
+  def public_recordings
+    recordings.where(visibility: [Recording::VISIBILITIES[:public], Recording::VISIBILITIES[:public_protected]])
+  end
+
   private
 
   def set_friendly_id
diff --git a/app/serializers/public_recording_serializer.rb b/app/serializers/public_recording_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a687d449dee5839dc88999e307d6ab6544a7cd2a
--- /dev/null
+++ b/app/serializers/public_recording_serializer.rb
@@ -0,0 +1,23 @@
+# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
+#
+# Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below).
+#
+# This program is free software; you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free Software
+# Foundation; either version 3.0 of the License, or (at your option) any later
+# version.
+#
+# Greenlight is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with Greenlight; if not, see <http://www.gnu.org/licenses/>.
+
+# frozen_string_literal: true
+
+class PublicRecordingSerializer < ApplicationSerializer
+  attributes :id, :record_id, :name, :length, :recorded_at
+
+  has_many :formats
+end
diff --git a/config/routes.rb b/config/routes.rb
index 33711f3b417896d9ecd5c3c1c99b958306a2431c..31b3881f023a54198f2f3b2469885b8bf54b3a85 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -45,6 +45,7 @@ Rails.application.routes.draw do
       resources :rooms, param: :friendly_id do
         member do
           get '/recordings', to: 'rooms#recordings'
+          get '/public_recordings', to: 'rooms#public_recordings'
           get '/recordings_processing', to: 'rooms#recordings_processing'
           get '/public', to: 'rooms#public_show'
           delete :purge_presentation
diff --git a/spec/controllers/rooms_controller_spec.rb b/spec/controllers/rooms_controller_spec.rb
index d86c9a3fdf5297b54113401f142c81d6949e9e47..7418355236e6bbbd829aef5aca87533d587f921e 100644
--- a/spec/controllers/rooms_controller_spec.rb
+++ b/spec/controllers/rooms_controller_spec.rb
@@ -327,4 +327,218 @@ RSpec.describe Api::V1::RoomsController, type: :controller do
       expect(recording_ids).to match_array(recordings.pluck(:id))
     end
   end
+
+  describe '#public_recordings' do
+    let(:room) { create(:room) }
+
+    before { sign_out_user }
+
+    context 'Filtration' do
+      let!(:public_recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:public]) }
+      let!(:public_protected_recording) { create(:recording, room:, visibility: Recording::VISIBILITIES[:public_protected]) }
+
+      before do
+        create(:recording, room:, visibility: Recording::VISIBILITIES[:unpublished])
+        create(:recording, room:, visibility: Recording::VISIBILITIES[:published])
+        create(:recording, room:, visibility: Recording::VISIBILITIES[:protected])
+      end
+
+      it 'returns :ok with a list of the room public recordings only' do
+        expected_response = JSON.parse(
+          [PublicRecordingSerializer.new(public_recording), PublicRecordingSerializer.new(public_protected_recording)].to_json
+        )
+
+        get :public_recordings, params: { friendly_id: room.friendly_id }
+
+        expect(response).to have_http_status(:ok)
+        expect(JSON.parse(response.body)['data']).to match_array(expected_response)
+      end
+    end
+
+    context 'Pagination' do
+      # The order of creation and the matching of :recorded_at value impacts a page recordings list.
+      # Thus fixing those values ensures the determinism of these examples.
+      let!(:first_page_recordings) do
+        create_list(:recording, Pagy::DEFAULT[:items], room:, recorded_at: Time.zone.at(1_686_943_664), visibility: Recording::VISIBILITIES[:public])
+      end
+      let!(:second_page_recordings) do
+        create_list(:recording, Pagy::DEFAULT[:items], room:, recorded_at: Time.zone.at(1_686_943_664),
+                                                       visibility: Recording::VISIBILITIES[:public_protected])
+      end
+
+      def expect_response_to_have(page:, pages:, recordings:)
+        expect(response).to have_http_status(:ok)
+        expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(recordings.pluck('id'))
+        expect(JSON.parse(response.body)['meta']['pages']).to eq(pages)
+        expect(JSON.parse(response.body)['meta']['page']).to eq(page)
+      end
+
+      context 'First page' do
+        it 'returns :ok with a list of the first page room public recordings' do
+          get :public_recordings, params: { friendly_id: room.friendly_id, page: 1 }
+
+          expect_response_to_have page: 1, pages: 2, recordings: first_page_recordings
+        end
+      end
+
+      context 'Second page' do
+        it 'returns :ok with a list of the first second page room public recordings' do
+          get :public_recordings, params: { friendly_id: room.friendly_id, page: 2 }
+
+          expect_response_to_have page: 2, pages: 2, recordings: second_page_recordings
+        end
+      end
+
+      context 'No page' do
+        it 'returns :ok with a list of the first page room public recordings' do
+          get :public_recordings, params: { friendly_id: room.friendly_id }
+
+          expect_response_to_have page: 1, pages: 2, recordings: first_page_recordings
+        end
+      end
+
+      context 'Overflowing page' do
+        it 'returns :ok with a list of the last page room public recordings' do
+          get :public_recordings, params: { friendly_id: room.friendly_id, page: 3 }
+
+          expect_response_to_have page: 2, pages: 2, recordings: second_page_recordings
+        end
+      end
+    end
+
+    context 'Sorting' do
+      # The order of creation and the choice of :recorded_at, :name and :length is not RANDOM and was carefully crafted.
+      # It was meant to have a scenario with high entropy.
+      # We decrease inter-records correlation for those values (especially respective to :created_at).
+      let!(:third_recorded_named_c_length_one) do
+        create(:recording, room:, name: 'C', recorded_at: Time.zone.at(1_686_943_800), length: 1, visibility: Recording::VISIBILITIES[:public])
+      end
+      let!(:second_recorded_named_a_length_three) do
+        create(:recording, room:, name: 'A', recorded_at: Time.zone.at(1_686_943_700), length: 3, visibility: Recording::VISIBILITIES[:public])
+      end
+      let!(:first_recorded_named_b_length_two) do
+        create(:recording, room:, name: 'B', recorded_at: Time.zone.at(1_686_943_600), length: 2, visibility: Recording::VISIBILITIES[:public])
+      end
+      let!(:last_recorded_named_c_length_one) do
+        create(:recording, room:, name: 'C', recorded_at: Time.zone.at(1_686_943_900), length: 1, visibility: Recording::VISIBILITIES[:public])
+      end
+
+      def expect_response_to_have_ordered(recordings:)
+        expect(response).to have_http_status(:ok)
+        expect(JSON.parse(response.body)['data'].pluck('id')).to eq(recordings.pluck('id'))
+      end
+
+      describe 'Sort by name' do
+        context 'ASC' do
+          it 'returns :ok with the list of the room public recordings sorted by name' do
+            get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'name', direction: 'ASC' } }
+
+            expect_response_to_have_ordered recordings: [second_recorded_named_a_length_three, first_recorded_named_b_length_two,
+                                                         last_recorded_named_c_length_one, third_recorded_named_c_length_one]
+          end
+        end
+
+        context 'DESC' do
+          it 'returns :ok with the list of the room public recordings sorted by name' do
+            get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'name', direction: 'DESC' } }
+
+            expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one,
+                                                         first_recorded_named_b_length_two, second_recorded_named_a_length_three]
+          end
+        end
+      end
+
+      describe 'Sort by length' do
+        context 'ASC' do
+          it 'returns :ok with the list of the room public recordings sorted by length' do
+            get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'length', direction: 'ASC' } }
+
+            expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one,
+                                                         first_recorded_named_b_length_two, second_recorded_named_a_length_three]
+          end
+        end
+
+        context 'DESC' do
+          it 'returns :ok with the list of the room public recordings sorted by length' do
+            get :public_recordings, params: { friendly_id: room.friendly_id, sort: { column: 'length', direction: 'DESC' } }
+
+            expect_response_to_have_ordered recordings: [second_recorded_named_a_length_three, first_recorded_named_b_length_two,
+                                                         last_recorded_named_c_length_one, third_recorded_named_c_length_one]
+          end
+        end
+      end
+
+      describe 'No sort by' do
+        it 'returns :ok with the list of the room public recordings sorted by recorded_at DESC' do
+          get :public_recordings, params: { friendly_id: room.friendly_id }
+
+          expect_response_to_have_ordered recordings: [last_recorded_named_c_length_one, third_recorded_named_c_length_one,
+                                                       second_recorded_named_a_length_three, first_recorded_named_b_length_two]
+        end
+      end
+    end
+
+    context 'Search' do
+      let!(:applied_math) do
+        create(:recording, room:, name: 'Applied mathematics', visibility: Recording::VISIBILITIES[:public])
+      end
+      let!(:advanced_math) do
+        create(:recording, room:, name: 'Advanced MaTHematics', visibility: Recording::VISIBILITIES[:public_protected])
+      end
+      let!(:thermodynamics) do
+        create(:recording, room:, name: 'Thermodynamics', visibility: Recording::VISIBILITIES[:public_protected])
+      end
+
+      def expect_response_to_match(recordings:)
+        expect(response).to have_http_status(:ok)
+        expect(JSON.parse(response.body)['data'].pluck('id')).to match_array(recordings.pluck('id'))
+      end
+
+      describe 'Matched by name' do
+        it 'returns :ok with the list of the room public recordings having matching name case insensitive' do
+          get :public_recordings, params: { friendly_id: room.friendly_id, search: 'math' }
+
+          expect_response_to_match recordings: [applied_math, advanced_math]
+        end
+      end
+
+      describe 'Matched by format type' do
+        before do
+          create(:format, recording: applied_math, recording_type: 'podcast')
+        end
+
+        it 'returns :ok with the list of the room public recordings having matching visibility case insensitive' do
+          get :public_recordings, params: { friendly_id: room.friendly_id, search: 'podcast' }
+
+          expect_response_to_match recordings: [applied_math]
+        end
+      end
+
+      describe 'No match' do
+        it 'returns :ok with an empty list' do
+          get :public_recordings,
+              params: { friendly_id: room.friendly_id, search: [Recording::VISIBILITIES[:public_protected], Recording::VISIBILITIES[:public]].sample }
+
+          expect_response_to_match recordings: []
+        end
+      end
+
+      describe 'No Search' do
+        it 'returns :ok with the list of the room public recordings' do
+          get :public_recordings, params: { friendly_id: room.friendly_id }
+
+          expect_response_to_match recordings: [applied_math, advanced_math, thermodynamics]
+        end
+      end
+    end
+
+    context 'Inexistent room' do
+      it 'returns :not_found' do
+        get :public_recordings, params: { friendly_id: '404' }
+
+        expect(response).to have_http_status(:not_found)
+        expect(JSON.parse(response.body)['data']).to be_blank
+      end
+    end
+  end
 end
diff --git a/spec/helpers.rb b/spec/helpers.rb
index 05f6b8f67404f0de027896ac085dbe46aa81fa80..6e679ec4a064a36dbd46e8219304f757b5c317a9 100644
--- a/spec/helpers.rb
+++ b/spec/helpers.rb
@@ -20,4 +20,8 @@ module Helpers
   def sign_in_user(user)
     session[:session_token] = user.session_token
   end
+
+  def sign_out_user
+    session[:session_token] = nil
+  end
 end
diff --git a/spec/models/recording_spec.rb b/spec/models/recording_spec.rb
index 952362f3b465690a5ff8c85f1b1aec0fc6fe3403..81a586f62fdf8dc0bae01d3cee144b21ab0c507d 100644
--- a/spec/models/recording_spec.rb
+++ b/spec/models/recording_spec.rb
@@ -77,4 +77,43 @@ RSpec.describe Recording, type: :model do
       expect(described_class.all.search('').pluck(:id)).to match_array(described_class.all.pluck(:id))
     end
   end
+
+  describe '#public_search' do
+    let(:recording1) { create(:recording, name: 'Greenlight 101', visibility: Recording::VISIBILITIES[:public]) }
+    let(:recording2) { create(:recording, name: 'Greenlight 201', visibility: Recording::VISIBILITIES[:public]) }
+    let(:recording3) { create(:recording, name: 'Bluelight 301', visibility: Recording::VISIBILITIES[:public]) }
+
+    before do
+      create_list(:recording, 5)
+      create(:format, recording: recording3, recording_type: 'podcast')
+    end
+
+    context 'Matching name' do
+      it 'returns the searched recordings' do
+        expect(described_class.public_search('greenlight')).to match_array([recording1, recording2])
+      end
+    end
+
+    context 'Matching format type' do
+      it 'returns the searched recordings' do
+        expect(described_class.public_search('podcast')).to match_array([recording3])
+      end
+    end
+
+    context 'Matching visibility' do
+      it 'returns an empty list' do
+        expect(described_class.public_search('public')).to be_empty
+      end
+    end
+
+    context 'No match' do
+      it 'returns an empty list' do
+        expect(described_class.public_search('404')).to be_empty
+      end
+    end
+
+    it 'returns all recordings if input is empty' do
+      expect(described_class.all.search('')).to match_array(described_class.all)
+    end
+  end
 end
diff --git a/spec/models/room_spec.rb b/spec/models/room_spec.rb
index 721c682184022c4e2150f57b4c9ad6df06f34c25..fca0f4f397c4389b5fd1e038516f2fbe408a235a 100644
--- a/spec/models/room_spec.rb
+++ b/spec/models/room_spec.rb
@@ -164,5 +164,24 @@ RSpec.describe Room, type: :model do
         expect(room.get_setting(name: '404')).to be_nil
       end
     end
+
+    describe '#public_recordings' do
+      let(:public_recordings) do
+        [
+          create(:recording, room:, visibility: Recording::VISIBILITIES[:public]),
+          create(:recording, room:, visibility: Recording::VISIBILITIES[:public_protected])
+        ]
+      end
+
+      before do
+        [Recording::VISIBILITIES[:unpublished], Recording::VISIBILITIES[:published], Recording::VISIBILITIES[:protected]].each do |visibility|
+          create(:recording, room:, visibility:)
+        end
+      end
+
+      it 'retuns filters out the room public recordings' do
+        expect(room.public_recordings).to match_array(public_recordings)
+      end
+    end
   end
 end